perp-cli 0.12.15 → 0.14.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 +278 -31
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +74 -84
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +33 -16
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spot adapter factory — the only place spot adapters are created (design §12.4 #1).
|
|
3
|
+
*
|
|
4
|
+
* - `getSpotAdapter` returns null iff the venue has no spot (`def.spot === null`).
|
|
5
|
+
* Creation / init / network errors propagate (SSOT Rule #2 — no catch).
|
|
6
|
+
* - `createSpot` (venue definition) constructs and init()s the adapter.
|
|
7
|
+
* - Cached per perp adapter instance (each instance is bound to one venue + network,
|
|
8
|
+
* so the key is the adapter's own network, not the process-wide one) and per
|
|
9
|
+
* on-chain-approve option. Read-only and signed perp adapters keep separate entries.
|
|
10
|
+
*
|
|
11
|
+
* Bot scope (§12.4 #3): bots only trade spot on venues where `isBotSpotVenue(def)`.
|
|
12
|
+
* Quote/base detection (§12.4 #6) goes through `def.spot.quoteAssets`.
|
|
13
|
+
*/
|
|
14
|
+
import { classifyError, extractErrorMessage, PerpError } from "../errors.js";
|
|
15
|
+
import { unsupported } from "./trading-v2.js";
|
|
16
|
+
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
17
|
+
import { canonicalFor, isNeverMatch } from "./canonical-symbols.js";
|
|
18
|
+
import { floorSize } from "./precision.js";
|
|
19
|
+
import { isSpotFreshBalancesCapable, isSpotHoldingPricesCapable, isSpotPricesCapable, isSpotTransferCapable, MAX_PRICE_DEVIATION_PCT } from "./spot-interface.js";
|
|
20
|
+
import { getVenue, listVenues, tryResolveVenueId } from "./venue.js";
|
|
21
|
+
import "./venues/index.js";
|
|
22
|
+
/** Options for every non-CLI caller: on-chain approve txs are never sent implicitly. */
|
|
23
|
+
export const NO_ONCHAIN_APPROVE = { confirmOnchainApprove: false };
|
|
24
|
+
/** perp adapter → `${venueId}:${confirmOnchainApprove}` → spot adapter. */
|
|
25
|
+
const _cache = new WeakMap();
|
|
26
|
+
/**
|
|
27
|
+
* spot adapter → (venue, perp adapter, network) it was created for; used by the transfer /
|
|
28
|
+
* quote helpers. `net` is the process's selected network (sharedApiNetwork — the -n the perp
|
|
29
|
+
* adapter was created with) and decides the spot quote (spotQuoteAssets).
|
|
30
|
+
*/
|
|
31
|
+
const _owner = new WeakMap();
|
|
32
|
+
function hasSpot(def) {
|
|
33
|
+
return def.spot !== null;
|
|
34
|
+
}
|
|
35
|
+
/** Venues with spot support, for remediation text. */
|
|
36
|
+
export function listSpotVenues() {
|
|
37
|
+
return listVenues().filter(hasSpot);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get (or create + init) the spot adapter for a venue. null iff the venue has no spot.
|
|
41
|
+
* `opts` defaults to NO_ONCHAIN_APPROVE (the safe deny); only the CLI passes --yes.
|
|
42
|
+
*/
|
|
43
|
+
export async function getSpotAdapter(venue, perp, opts = NO_ONCHAIN_APPROVE) {
|
|
44
|
+
const def = getVenue(venue);
|
|
45
|
+
if (!hasSpot(def))
|
|
46
|
+
return null;
|
|
47
|
+
let perPerp = _cache.get(perp);
|
|
48
|
+
if (perPerp === undefined) {
|
|
49
|
+
perPerp = new Map();
|
|
50
|
+
_cache.set(perp, perPerp);
|
|
51
|
+
}
|
|
52
|
+
const key = `${def.id}:${opts.confirmOnchainApprove}`;
|
|
53
|
+
const hit = perPerp.get(key);
|
|
54
|
+
if (hit !== undefined)
|
|
55
|
+
return hit;
|
|
56
|
+
const net = sharedApiNetwork();
|
|
57
|
+
const spot = def.spot.createSpot(perp, opts).then(s => {
|
|
58
|
+
_owner.set(s, { def, perp, net });
|
|
59
|
+
return s;
|
|
60
|
+
});
|
|
61
|
+
perPerp.set(key, spot);
|
|
62
|
+
try {
|
|
63
|
+
return await spot;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
// Do not cache a failed creation; the error itself propagates.
|
|
67
|
+
if (perPerp.get(key) === spot)
|
|
68
|
+
perPerp.delete(key);
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** The venue's spot definition, or CAPABILITY_UNSUPPORTED. */
|
|
73
|
+
export function requireSpotDefinition(def) {
|
|
74
|
+
if (!hasSpot(def)) {
|
|
75
|
+
throw unsupported(def.displayName, "spot", "the venue has no spot trading", `Spot venues: ${listSpotVenues().map(v => v.id).join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
return def.spot;
|
|
78
|
+
}
|
|
79
|
+
/** CLI form: throws CAPABILITY_UNSUPPORTED when the venue has no spot. */
|
|
80
|
+
export async function requireSpotAdapter(venue, perp, opts = NO_ONCHAIN_APPROVE) {
|
|
81
|
+
requireSpotDefinition(getVenue(venue));
|
|
82
|
+
const spot = await getSpotAdapter(venue, perp, opts);
|
|
83
|
+
if (spot === null)
|
|
84
|
+
throw new Error(`unreachable: ${venue} spot definition present but no adapter`);
|
|
85
|
+
return spot;
|
|
86
|
+
}
|
|
87
|
+
// ── Bot scope (§12.4 #3) ──
|
|
88
|
+
/**
|
|
89
|
+
* Bots (funding-arb*, spot-perp-*, arb-auto, arb/index) only use spot venues with a
|
|
90
|
+
* separate (or runtime-decided) spot balance and a spot↔perp transfer. Pacifica
|
|
91
|
+
* (shared margin) and Arcus (on-chain swap) are excluded.
|
|
92
|
+
*/
|
|
93
|
+
export function isBotSpotVenue(def) {
|
|
94
|
+
return def.spot !== null
|
|
95
|
+
&& (def.spot.accountModel === "separate" || def.spot.accountModel === "runtime")
|
|
96
|
+
&& def.spot.capabilities.transfer;
|
|
97
|
+
}
|
|
98
|
+
export function listBotSpotVenues() {
|
|
99
|
+
return listVenues().filter(isBotSpotVenue);
|
|
100
|
+
}
|
|
101
|
+
/** Bot form: throws CAPABILITY_UNSUPPORTED for venues outside the bot spot scope. */
|
|
102
|
+
export function requireBotSpotVenue(venue) {
|
|
103
|
+
const def = getVenue(venue);
|
|
104
|
+
if (!hasSpot(def) || !isBotSpotVenue(def)) {
|
|
105
|
+
throw unsupported(def.displayName, "botSpot", "bots/arb need a separate spot balance with spot↔perp transfer", `Bot spot venues: ${listBotSpotVenues().map(v => v.id).join(", ")}. Trade ${def.id} spot manually: perp -e ${def.id} trade spot-buy ...`);
|
|
106
|
+
}
|
|
107
|
+
return def;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Bot spot adapter for an adapter-map name. null when the name is not a venue, or the
|
|
111
|
+
* venue is outside the bot spot scope. Creation errors propagate.
|
|
112
|
+
*/
|
|
113
|
+
export async function getBotSpotAdapter(name, perp) {
|
|
114
|
+
const id = tryResolveVenueId(name);
|
|
115
|
+
if (id === null || !isBotSpotVenue(getVenue(id)))
|
|
116
|
+
return null;
|
|
117
|
+
return getSpotAdapter(id, perp);
|
|
118
|
+
}
|
|
119
|
+
// ── Quote / base helpers (§12.4 #6) ──
|
|
120
|
+
/** Upper-cased token with venue balance suffixes removed ("USDC_SPOT" / "PURR-SPOT" → base). */
|
|
121
|
+
export function spotTokenBase(token) {
|
|
122
|
+
return token.toUpperCase().replace(/[-_]SPOT$/, "");
|
|
123
|
+
}
|
|
124
|
+
/** The venue's spot quote assets on `net` (network-keyed: Lighter RH quotes USDG on mainnet, USDC on testnet). */
|
|
125
|
+
export function spotQuoteAssets(def, net) {
|
|
126
|
+
return requireSpotDefinition(def).quoteAssets[net];
|
|
127
|
+
}
|
|
128
|
+
/** Default quote on `net` (bare-base symbols, transfers). */
|
|
129
|
+
export function defaultSpotQuote(def, net) {
|
|
130
|
+
const quote = spotQuoteAssets(def, net)[0];
|
|
131
|
+
if (quote === undefined)
|
|
132
|
+
throw new Error(`${def.id} spot definition has no ${net} quoteAssets`);
|
|
133
|
+
return quote;
|
|
134
|
+
}
|
|
135
|
+
/** The network a spot adapter was created for (getSpotAdapter). */
|
|
136
|
+
export function spotNetworkOf(spot) {
|
|
137
|
+
return ownerOf(spot).net;
|
|
138
|
+
}
|
|
139
|
+
export function isSpotQuoteAsset(def, net, token) {
|
|
140
|
+
return spotQuoteAssets(def, net).includes(spotTokenBase(token));
|
|
141
|
+
}
|
|
142
|
+
/** "BASE/QUOTE" for a bare base on this venue (quoteAssets[net][0]). */
|
|
143
|
+
export function spotPairSymbol(def, net, base) {
|
|
144
|
+
return `${base.toUpperCase()}/${defaultSpotQuote(def, net)}`;
|
|
145
|
+
}
|
|
146
|
+
/** The balance row of the venue's default quote asset (transfer asset), or null. */
|
|
147
|
+
export function findSpotQuoteBalance(def, net, balances) {
|
|
148
|
+
const quote = defaultSpotQuote(def, net);
|
|
149
|
+
return balances.find(b => spotTokenBase(b.token) === quote) ?? null;
|
|
150
|
+
}
|
|
151
|
+
/** Non-quote balances with a positive total (the "holdings" side of spot-perp hedges). */
|
|
152
|
+
export function nonQuoteSpotBalances(def, net, balances) {
|
|
153
|
+
return balances.filter(b => Number(b.total) > 0 && !isSpotQuoteAsset(def, net, b.token));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* true when spot quote balance is the same money as perp margin for this perp
|
|
157
|
+
* adapter: accountModel "unified", or "runtime" with a unified perp account (HL).
|
|
158
|
+
*/
|
|
159
|
+
export function spotSharesPerpMargin(def, perp) {
|
|
160
|
+
const spot = requireSpotDefinition(def);
|
|
161
|
+
if (spot.accountModel === "unified")
|
|
162
|
+
return true;
|
|
163
|
+
if (spot.accountModel === "runtime")
|
|
164
|
+
return perp.isUnifiedAccount === true;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* USD value of positive spot balances. Quote assets (stablecoins) are 1:1; other
|
|
169
|
+
* tokens use the venue's spot mark price. Tokens without a usable price are
|
|
170
|
+
* EXCLUDED and reported via `warn` — never valued at 0 (SSOT Rule #2).
|
|
171
|
+
* `shared` is carried through so aggregators can skip money already in perp equity.
|
|
172
|
+
*/
|
|
173
|
+
export function valueSpotBalances(def, net, balances, markets, warn) {
|
|
174
|
+
// markPrice null = no usable price (§12 SpotMarketInfo) → the token is excluded below.
|
|
175
|
+
const prices = new Map(markets.flatMap(m => (m.markPrice === null ? [] : [[m.baseToken.toUpperCase(), Number(m.markPrice)]])));
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const b of balances) {
|
|
178
|
+
const total = Number(b.total);
|
|
179
|
+
if (!(total > 0))
|
|
180
|
+
continue;
|
|
181
|
+
if (isSpotQuoteAsset(def, net, b.token)) {
|
|
182
|
+
out.push({ ...b, valueUsd: total });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const px = prices.get(spotTokenBase(b.token));
|
|
186
|
+
if (px === undefined || !Number.isFinite(px) || px <= 0) {
|
|
187
|
+
warn(`${def.displayName} spot ${b.token}: no market price — excluded from value totals`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
out.push({ ...b, valueUsd: px * total });
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/** Timeout for one venue's spot valuation (balances + prices) in aggregate views (portfolio, landing). */
|
|
195
|
+
export const SPOT_VALUATION_TIMEOUT_MS = 5_000;
|
|
196
|
+
/**
|
|
197
|
+
* Prices used to value `balances`. Wallet-model venues (Arcus) price only the held
|
|
198
|
+
* non-quote tokens (`getSpotPrices`); exchange venues use `getSpotMarkets()`, plus — when the
|
|
199
|
+
* adapter is SpotHoldingPricesCapable — `getHoldingPrices()` for held tokens that are not a spot
|
|
200
|
+
* market (Hyperliquid HIP-4 outcome tokens). A holding price counts only when it is quoted in one of
|
|
201
|
+
* the venue's spot quote assets (valued 1:1 like the quote balance); any other quote (or an unknown
|
|
202
|
+
* one) → markPrice null → the token stays unpriced (never 0).
|
|
203
|
+
*/
|
|
204
|
+
export async function spotMarketsForValuation(def, spot, balances, net) {
|
|
205
|
+
if (requireSpotDefinition(def).accountModel !== "wallet") {
|
|
206
|
+
const markets = await spot.getSpotMarkets();
|
|
207
|
+
if (!isSpotHoldingPricesCapable(spot))
|
|
208
|
+
return markets;
|
|
209
|
+
const listed = new Set(markets.map(m => m.baseToken.toUpperCase()));
|
|
210
|
+
const unlisted = nonQuoteSpotBalances(def, net, balances).map(b => spotTokenBase(b.token)).filter(t => !listed.has(t));
|
|
211
|
+
if (unlisted.length === 0)
|
|
212
|
+
return markets;
|
|
213
|
+
const quotes = spotQuoteAssets(def, net);
|
|
214
|
+
const held = (await spot.getHoldingPrices(unlisted)).map(p => ({
|
|
215
|
+
baseToken: p.token,
|
|
216
|
+
markPrice: p.quoteToken !== null && quotes.includes(p.quoteToken.toUpperCase()) ? p.markPrice : null,
|
|
217
|
+
}));
|
|
218
|
+
return [...markets, ...held];
|
|
219
|
+
}
|
|
220
|
+
if (!isSpotPricesCapable(spot)) {
|
|
221
|
+
throw new Error(`${def.id} is a wallet-model spot venue but its adapter has no getSpotPrices()`);
|
|
222
|
+
}
|
|
223
|
+
const held = nonQuoteSpotBalances(def, net, balances).map(b => spotTokenBase(b.token));
|
|
224
|
+
return held.length === 0 ? [] : spot.getSpotPrices(held);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Balances + valuation for aggregate views, bounded by `timeoutMs` (rejects with a
|
|
228
|
+
* TIMEOUT error; the caller reports it — never a partial or zero value).
|
|
229
|
+
*/
|
|
230
|
+
export async function fetchValuedSpotBalances(def, spot, warn, timeoutMs = SPOT_VALUATION_TIMEOUT_MS) {
|
|
231
|
+
return withSpotTimeout((async () => {
|
|
232
|
+
const net = spotNetworkOf(spot);
|
|
233
|
+
const balances = await spot.getSpotBalances();
|
|
234
|
+
const markets = await spotMarketsForValuation(def, spot, balances, net);
|
|
235
|
+
return valueSpotBalances(def, net, balances, markets, warn);
|
|
236
|
+
})(), `${def.displayName} spot valuation`, timeoutMs);
|
|
237
|
+
}
|
|
238
|
+
/** Reject with TIMEOUT after `timeoutMs` (the timer is cleared either way). */
|
|
239
|
+
export async function withSpotTimeout(p, what, timeoutMs = SPOT_VALUATION_TIMEOUT_MS) {
|
|
240
|
+
let timer;
|
|
241
|
+
const timeout = new Promise((_, rej) => {
|
|
242
|
+
timer = setTimeout(() => rej(new PerpError("TIMEOUT", `${what} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
return await Promise.race([p, timeout]);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ── Transfers (§12.4 #4) ──
|
|
252
|
+
function ownerOf(spot) {
|
|
253
|
+
const o = _owner.get(spot);
|
|
254
|
+
if (!o)
|
|
255
|
+
throw new Error(`spot adapter "${spot.name}" was not created by getSpotAdapter()`);
|
|
256
|
+
return o;
|
|
257
|
+
}
|
|
258
|
+
async function transferQuote(spot, amount, to) {
|
|
259
|
+
const { def, perp, net } = ownerOf(spot);
|
|
260
|
+
if (spotSharesPerpMargin(def, perp))
|
|
261
|
+
return false;
|
|
262
|
+
if (!def.spot.capabilities.transfer || !isSpotTransferCapable(spot)) {
|
|
263
|
+
throw unsupported(def.displayName, "spotTransfer", `spot account model is "${def.spot.accountModel}"`, `${def.id}: fund spot directly`);
|
|
264
|
+
}
|
|
265
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
266
|
+
throw new PerpError("INVALID_PARAMS", `Transfer amount must be positive (got ${amount})`);
|
|
267
|
+
}
|
|
268
|
+
const asset = defaultSpotQuote(def, net);
|
|
269
|
+
const wire = transferAmountString(amount);
|
|
270
|
+
if (to === "spot")
|
|
271
|
+
await spot.transferToSpot(wire, asset);
|
|
272
|
+
else
|
|
273
|
+
await spot.transferToPerp(wire, asset);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
/** Quote-asset transfer grid (USDC / USDT / USDG are 6-decimal tokens). */
|
|
277
|
+
const TRANSFER_STEP = "0.000001";
|
|
278
|
+
/**
|
|
279
|
+
* Fixed-point decimal string for a transfer amount, floored to TRANSFER_STEP — never
|
|
280
|
+
* `String(n)` (exponent "1e-7" or float artefacts "0.30000000000000004").
|
|
281
|
+
*/
|
|
282
|
+
export function transferAmountString(amount) {
|
|
283
|
+
// toFixed(12) is plain decimal for any amount below 1e21; floorSize re-checks the form.
|
|
284
|
+
return floorSize(amount.toFixed(12), TRANSFER_STEP);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Move the venue's quote asset (quoteAssets[0]) perp → spot.
|
|
288
|
+
* Returns false (no-op, nothing moved) when spot and perp share margin; throws
|
|
289
|
+
* CAPABILITY_UNSUPPORTED when the venue has no transfer. Callers log "Transferred" only on true.
|
|
290
|
+
*/
|
|
291
|
+
export function transferQuoteToSpot(spot, amount) {
|
|
292
|
+
return transferQuote(spot, amount, "spot");
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* A spot↔perp transfer the venue refused for lack of funds (classifyError → INSUFFICIENT_BALANCE,
|
|
296
|
+
* e.g. HL "Insufficient balance for withdrawal.") → INSUFFICIENT_BALANCE that says how much the
|
|
297
|
+
* SOURCE side can move OF THE TRANSFER ASSET (`readAvailable`: perp withdrawable for to-spot, spot
|
|
298
|
+
* quote available for to-perp) — never an account-wide equity figure: a multi-asset margin
|
|
299
|
+
* account can hold margin and none of the asset. A failed read is reported as such (never a
|
|
300
|
+
* guessed figure). Any other error → unchanged.
|
|
301
|
+
*/
|
|
302
|
+
export async function transferShortfallError(e, opts) {
|
|
303
|
+
if (classifyError(e).code !== "INSUFFICIENT_BALANCE")
|
|
304
|
+
return e;
|
|
305
|
+
const source = opts.direction === "to-spot" ? "perp" : "spot";
|
|
306
|
+
const from = opts.direction === "to-spot" ? "perp withdrawable" : "spot available";
|
|
307
|
+
let have;
|
|
308
|
+
let available = null;
|
|
309
|
+
try {
|
|
310
|
+
const n = await opts.readAvailable();
|
|
311
|
+
if (!Number.isFinite(n))
|
|
312
|
+
throw new Error(`venue reported a non-numeric balance (${String(n)})`);
|
|
313
|
+
// transferAmountString refuses 0 (it formats an amount to SEND); nothing to move is "0".
|
|
314
|
+
available = n >= Number(TRANSFER_STEP) ? transferAmountString(n) : "0";
|
|
315
|
+
have = `${from} is ${available} ${opts.asset}`;
|
|
316
|
+
}
|
|
317
|
+
catch (re) {
|
|
318
|
+
have = `${from} could not be read (${extractErrorMessage(re)})`;
|
|
319
|
+
}
|
|
320
|
+
return new PerpError("INSUFFICIENT_BALANCE", `${opts.venue} ${opts.direction} transfer of ${opts.amount} ${opts.asset} refused: ${extractErrorMessage(e)} — ${have}`, {
|
|
321
|
+
exchange: opts.venue, requested: opts.amount, available, asset: opts.asset,
|
|
322
|
+
remediation: available === null
|
|
323
|
+
? `Check balances: perp -e ${opts.venue} portfolio / trade spot-balance`
|
|
324
|
+
: Number(available) > 0
|
|
325
|
+
? `Transfer at most ${available} ${opts.asset}, or fund the ${source} account first`
|
|
326
|
+
: `The ${source} account holds no transferable ${opts.asset} — this command moves ${opts.asset} only (margin held in another asset is not ${opts.asset}). Fund the ${source} account with ${opts.asset} first; check: perp -e ${opts.venue} portfolio / trade spot-balance`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** Spot → perp counterpart of transferQuoteToSpot. */
|
|
330
|
+
export function transferQuoteToPerp(spot, amount) {
|
|
331
|
+
return transferQuote(spot, amount, "perp");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Spot quote (quoteAssets[0]) `available` read UNCACHED; 0 when the venue holds no
|
|
335
|
+
* quote row. Errors propagate. Used as the baseline for sweepSpotQuoteToPerp().
|
|
336
|
+
*/
|
|
337
|
+
export async function readSpotQuoteAvailableFresh(spot) {
|
|
338
|
+
const { def, net } = ownerOf(spot);
|
|
339
|
+
const bals = isSpotFreshBalancesCapable(spot) ? await spot.getSpotBalancesFresh() : await spot.getSpotBalances();
|
|
340
|
+
const bal = findSpotQuoteBalance(def, net, bals);
|
|
341
|
+
if (bal === null)
|
|
342
|
+
return 0;
|
|
343
|
+
const n = Number(bal.available);
|
|
344
|
+
if (!Number.isFinite(n))
|
|
345
|
+
throw new PerpError("EXCHANGE_ERROR", `${spot.name} spot ${bal.token} available is not a number: ${bal.available}`, { exchange: spot.name });
|
|
346
|
+
return n;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Sweep back to perp ONLY the spot quote this trade produced / left over: the part of
|
|
350
|
+
* the (uncached) available balance above `keep` (the quote that was there before the
|
|
351
|
+
* trade — never touched), capped at `max` (e.g. what this trade transferred in; null =
|
|
352
|
+
* no cap). Returns the amount moved; 0 when the balance is shared with perp or the
|
|
353
|
+
* amount is ≤ minAmount. Errors propagate — callers surface them (funds remain in spot).
|
|
354
|
+
*/
|
|
355
|
+
export async function sweepSpotQuoteToPerp(spot, minAmount, opts) {
|
|
356
|
+
const { def, perp, net } = ownerOf(spot);
|
|
357
|
+
if (spotSharesPerpMargin(def, perp))
|
|
358
|
+
return 0;
|
|
359
|
+
const bals = isSpotFreshBalancesCapable(spot) ? await spot.getSpotBalancesFresh() : await spot.getSpotBalances();
|
|
360
|
+
const bal = findSpotQuoteBalance(def, net, bals);
|
|
361
|
+
if (bal === null || bal.shared)
|
|
362
|
+
return 0;
|
|
363
|
+
const available = Number(bal.available);
|
|
364
|
+
if (!Number.isFinite(available))
|
|
365
|
+
throw new PerpError("EXCHANGE_ERROR", `${spot.name} spot ${bal.token} available is not a number: ${bal.available}`, { exchange: spot.name });
|
|
366
|
+
let movable = available - opts.keep;
|
|
367
|
+
if (opts.max !== null)
|
|
368
|
+
movable = Math.min(movable, opts.max);
|
|
369
|
+
const amount = Math.floor(movable * 100) / 100;
|
|
370
|
+
if (!(amount > minAmount))
|
|
371
|
+
return 0;
|
|
372
|
+
return (await transferQuoteToPerp(spot, amount)) ? amount : 0;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Match a spot venue's markets (default quote only) to a perp venue's markets by
|
|
376
|
+
* canonical symbol — `canonicalFor(spotVenue, base, perp.assetClass)` must equal the
|
|
377
|
+
* perp market's canonical (`null` never matches, NEVER_MATCH pairs excluded) — plus a
|
|
378
|
+
* price guard: |spot − perp mark| ≤ MAX_PRICE_DEVIATION_PCT. Spot markets without a
|
|
379
|
+
* price cannot be validated and are not matched. Bare-ticker equality is never enough.
|
|
380
|
+
*
|
|
381
|
+
* `perpMarkets` = the perp venue's public markets (`fetchPerpMarketsForMatching`).
|
|
382
|
+
* Returns spot base (upper-case) → match.
|
|
383
|
+
*/
|
|
384
|
+
export function matchSpotPerp(spotVenue, spotMarkets, perpVenue, perpMarkets, warn, net) {
|
|
385
|
+
const spotDef = getVenue(spotVenue);
|
|
386
|
+
const perpDef = getVenue(perpVenue);
|
|
387
|
+
const quote = defaultSpotQuote(spotDef, net);
|
|
388
|
+
const classes = new Set(perpMarkets.map(m => m.assetClass));
|
|
389
|
+
// (assetClass, canonical) → perp market
|
|
390
|
+
const perpByKey = new Map();
|
|
391
|
+
for (const m of perpMarkets) {
|
|
392
|
+
if (m.canonical !== null)
|
|
393
|
+
perpByKey.set(`${m.assetClass}|${m.canonical}`, m);
|
|
394
|
+
}
|
|
395
|
+
const out = new Map();
|
|
396
|
+
const deviating = [];
|
|
397
|
+
for (const sm of spotMarkets) {
|
|
398
|
+
if (sm.quoteToken.toUpperCase() !== quote || sm.markPrice === null)
|
|
399
|
+
continue;
|
|
400
|
+
const spotPrice = Number(sm.markPrice);
|
|
401
|
+
if (!Number.isFinite(spotPrice) || spotPrice <= 0)
|
|
402
|
+
continue;
|
|
403
|
+
const base = sm.baseToken.toUpperCase();
|
|
404
|
+
for (const cls of classes) {
|
|
405
|
+
const canonical = canonicalFor(spotDef.id, base, cls);
|
|
406
|
+
if (canonical === null)
|
|
407
|
+
continue;
|
|
408
|
+
const pm = perpByKey.get(`${cls}|${canonical}`);
|
|
409
|
+
if (pm === undefined || isNeverMatch(base, pm.symbol.toUpperCase()))
|
|
410
|
+
continue;
|
|
411
|
+
const perpPrice = Number(pm.markPrice);
|
|
412
|
+
if (!Number.isFinite(perpPrice) || perpPrice <= 0)
|
|
413
|
+
continue;
|
|
414
|
+
const deviationPct = Math.abs(spotPrice - perpPrice) / perpPrice * 100;
|
|
415
|
+
if (deviationPct > MAX_PRICE_DEVIATION_PCT) {
|
|
416
|
+
deviating.push(`${base}/${quote}↔${pm.symbol} ${deviationPct.toFixed(1)}%`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
out.set(base, { spotBase: base, perpSymbol: pm.symbol, canonical, assetClass: cls, spotPrice, perpPrice, deviationPct });
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (deviating.length > 0) {
|
|
424
|
+
warn(`${spotDef.displayName} spot ↔ ${perpDef.displayName} perp: ${deviating.length} same-symbol pair(s) excluded, price deviation > ${MAX_PRICE_DEVIATION_PCT}%: ${deviating.join(", ")}`);
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
/** The perp venue's public markets (canonical + asset class + mark) on `net` (default: process network). */
|
|
429
|
+
export function fetchPerpMarketsForMatching(perpVenue, net = sharedApiNetwork()) {
|
|
430
|
+
return getVenue(perpVenue).publicData.fetchMarkets(net);
|
|
431
|
+
}
|
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
* Separate from ExchangeAdapter because spot uses balance-based tracking
|
|
4
4
|
* (not position-based) and has no leverage, liquidation, or funding concepts.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Venues declare spot support in their VenueDefinition (`spot`, design §12);
|
|
7
|
+
* adapters are created only through `getSpotAdapter()` in spot-factory.ts.
|
|
8
|
+
*
|
|
9
|
+
* Symbol rule (§12.4 #6): `SpotMarketInfo.symbol` is "BASE/QUOTE". Adapter
|
|
10
|
+
* methods accept a bare base ("ETH" → BASE/quoteAssets[0]) or "BASE/QUOTE"
|
|
11
|
+
* and translate to the venue's wire form.
|
|
7
12
|
*/
|
|
13
|
+
import type { ResolvedSlippage } from "./slippage.js";
|
|
8
14
|
export interface SpotMarketInfo {
|
|
9
15
|
symbol: string;
|
|
10
16
|
baseToken: string;
|
|
11
17
|
quoteToken: string;
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
/**
|
|
19
|
+
* null = the venue has no usable price for this market right now (no mark and no
|
|
20
|
+
* two-sided book). Never "0" or a stale last trade (Rule #2). Consumers skip it for
|
|
21
|
+
* valuation / matching.
|
|
22
|
+
*/
|
|
23
|
+
markPrice: string | null;
|
|
24
|
+
volume24h: string | null;
|
|
14
25
|
sizeDecimals: number;
|
|
15
26
|
priceDecimals: number;
|
|
16
27
|
}
|
|
@@ -20,7 +31,87 @@ export interface SpotBalance {
|
|
|
20
31
|
available: string;
|
|
21
32
|
held: string;
|
|
22
33
|
entryNtl?: string;
|
|
34
|
+
/**
|
|
35
|
+
* true = this balance is the same money as the perp account (Pacifica USDC,
|
|
36
|
+
* HL unified). Aggregators must not add it on top of perp equity (§12.4 #8).
|
|
37
|
+
*/
|
|
38
|
+
shared: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Normalized spot order result for venues added under §12 (legacy HL/Lighter return unknown). */
|
|
41
|
+
export interface SpotOrderResult {
|
|
42
|
+
venue: string;
|
|
43
|
+
symbol: string;
|
|
44
|
+
orderId: string | null;
|
|
45
|
+
status: "filled" | "partial" | "open" | "pending" | "rejected" | "cancelled";
|
|
46
|
+
/** null = venue did not report it */
|
|
47
|
+
filledSize: string | null;
|
|
48
|
+
avgPrice: string | null;
|
|
49
|
+
/** "pending" = submitted but not confirmed final — do NOT resubmit. */
|
|
50
|
+
finality: "final" | "pending";
|
|
51
|
+
/** Token actually received (Arcus router may deliver a wrapped token); null when not applicable. */
|
|
52
|
+
receivedToken: string | null;
|
|
53
|
+
remediation: string | null;
|
|
54
|
+
venueRaw: unknown;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `venueRaw.fillSource` marker for a fill parsed from a tx event (Lighter event_info
|
|
58
|
+
* `is` − `rs`): spotMarketFill() cross-checks such fills against a fresh balance delta.
|
|
59
|
+
*/
|
|
60
|
+
export declare const TX_EVENT_FILL_SOURCE = "tx-event-info";
|
|
61
|
+
/** Spot ↔ perp transfer for `accountModel: "separate" | "runtime"` venues. Amount is a decimal string. */
|
|
62
|
+
export interface SpotTransferCapable {
|
|
63
|
+
transferToSpot(amount: string, asset: string): Promise<unknown>;
|
|
64
|
+
transferToPerp(amount: string, asset: string): Promise<unknown>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Uncached balance read. REQUIRED for any adapter whose getSpotBalances() is served
|
|
68
|
+
* from the shared TTL cache (src/cache.ts `acct:` keys — Lighter, Hyperliquid): fill
|
|
69
|
+
* measurement and post-trade verification must never see a pre-trade cached row.
|
|
70
|
+
* Adapters without it read live on every getSpotBalances() call.
|
|
71
|
+
*/
|
|
72
|
+
export interface SpotFreshBalancesCapable {
|
|
73
|
+
getSpotBalancesFresh(): Promise<SpotBalance[]>;
|
|
74
|
+
}
|
|
75
|
+
export declare function isSpotFreshBalancesCapable(a: SpotAdapter): a is SpotAdapter & SpotFreshBalancesCapable;
|
|
76
|
+
export interface SpotOpenOrdersCapable {
|
|
77
|
+
getSpotOpenOrders(symbol: string | null): Promise<Array<{
|
|
78
|
+
orderId: string;
|
|
79
|
+
symbol: string;
|
|
80
|
+
side: "buy" | "sell";
|
|
81
|
+
price: string;
|
|
82
|
+
size: string;
|
|
83
|
+
filled: string;
|
|
84
|
+
}>>;
|
|
23
85
|
}
|
|
86
|
+
export interface SpotCancelAllCapable {
|
|
87
|
+
spotCancelAllOrders(symbol: string | null): Promise<unknown>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Price only the given base tokens (same SpotMarketInfo shape as getSpotMarkets).
|
|
91
|
+
* Required for `accountModel: "wallet"` venues, where pricing the whole market
|
|
92
|
+
* universe is too slow for valuation (Arcus: one router /price call per token).
|
|
93
|
+
*/
|
|
94
|
+
export interface SpotPricesCapable {
|
|
95
|
+
getSpotPrices(tokens: readonly string[]): Promise<SpotMarketInfo[]>;
|
|
96
|
+
}
|
|
97
|
+
export declare function isSpotPricesCapable(a: SpotAdapter): a is SpotAdapter & SpotPricesCapable;
|
|
98
|
+
/**
|
|
99
|
+
* A held token's price for valuation only — a token the venue holds in spot but does not list as a
|
|
100
|
+
* spot market (Hyperliquid HIP-4 outcome tokens "+<encoding>"). `quoteToken` = the token its price is
|
|
101
|
+
* quoted in; null = unknown (then the price is unusable). `markPrice` null = no usable price (Rule #2).
|
|
102
|
+
*/
|
|
103
|
+
export interface SpotHoldingPrice {
|
|
104
|
+
token: string;
|
|
105
|
+
quoteToken: string | null;
|
|
106
|
+
markPrice: string | null;
|
|
107
|
+
}
|
|
108
|
+
/** Prices for held tokens without a spot market (valuation: portfolio, bot equity). Unpriceable tokens may be omitted. */
|
|
109
|
+
export interface SpotHoldingPricesCapable {
|
|
110
|
+
getHoldingPrices(tokens: readonly string[]): Promise<SpotHoldingPrice[]>;
|
|
111
|
+
}
|
|
112
|
+
export declare function isSpotHoldingPricesCapable(a: SpotAdapter): a is SpotAdapter & SpotHoldingPricesCapable;
|
|
113
|
+
export declare function isSpotTransferCapable(a: SpotAdapter): a is SpotAdapter & SpotTransferCapable;
|
|
114
|
+
export declare function isSpotOpenOrdersCapable(a: SpotAdapter): a is SpotAdapter & SpotOpenOrdersCapable;
|
|
24
115
|
export interface SpotOrderParams {
|
|
25
116
|
symbol: string;
|
|
26
117
|
side: "buy" | "sell";
|
|
@@ -38,7 +129,10 @@ export interface SpotAdapter {
|
|
|
38
129
|
asks: [string, string][];
|
|
39
130
|
}>;
|
|
40
131
|
getSpotBalances(): Promise<SpotBalance[]>;
|
|
41
|
-
|
|
132
|
+
/** Market order within the resolved slippage bound (design §13.2 — same rule as perps). */
|
|
133
|
+
spotMarketOrder(symbol: string, side: "buy" | "sell", size: string, opts: {
|
|
134
|
+
slippage: ResolvedSlippage;
|
|
135
|
+
}): Promise<unknown>;
|
|
42
136
|
spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
|
|
43
137
|
tif?: string;
|
|
44
138
|
}): Promise<unknown>;
|
|
@@ -3,8 +3,34 @@
|
|
|
3
3
|
* Separate from ExchangeAdapter because spot uses balance-based tracking
|
|
4
4
|
* (not position-based) and has no leverage, liquidation, or funding concepts.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Venues declare spot support in their VenueDefinition (`spot`, design §12);
|
|
7
|
+
* adapters are created only through `getSpotAdapter()` in spot-factory.ts.
|
|
8
|
+
*
|
|
9
|
+
* Symbol rule (§12.4 #6): `SpotMarketInfo.symbol` is "BASE/QUOTE". Adapter
|
|
10
|
+
* methods accept a bare base ("ETH" → BASE/quoteAssets[0]) or "BASE/QUOTE"
|
|
11
|
+
* and translate to the venue's wire form.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* `venueRaw.fillSource` marker for a fill parsed from a tx event (Lighter event_info
|
|
15
|
+
* `is` − `rs`): spotMarketFill() cross-checks such fills against a fresh balance delta.
|
|
7
16
|
*/
|
|
17
|
+
export const TX_EVENT_FILL_SOURCE = "tx-event-info";
|
|
18
|
+
export function isSpotFreshBalancesCapable(a) {
|
|
19
|
+
return typeof a.getSpotBalancesFresh === "function";
|
|
20
|
+
}
|
|
21
|
+
export function isSpotPricesCapable(a) {
|
|
22
|
+
return typeof a.getSpotPrices === "function";
|
|
23
|
+
}
|
|
24
|
+
export function isSpotHoldingPricesCapable(a) {
|
|
25
|
+
return typeof a.getHoldingPrices === "function";
|
|
26
|
+
}
|
|
27
|
+
export function isSpotTransferCapable(a) {
|
|
28
|
+
const r = a;
|
|
29
|
+
return typeof r.transferToSpot === "function" && typeof r.transferToPerp === "function";
|
|
30
|
+
}
|
|
31
|
+
export function isSpotOpenOrdersCapable(a) {
|
|
32
|
+
return typeof a.getSpotOpenOrders === "function";
|
|
33
|
+
}
|
|
8
34
|
// ── Token mapping & price verification ──
|
|
9
35
|
/**
|
|
10
36
|
* U-token (Unit protocol) → underlying perp symbol mapping.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spot order outcome → filled size, for bots / arb (design §12.4 #5).
|
|
3
|
+
*
|
|
4
|
+
* A resolved `spotMarketOrder()` promise does NOT mean "filled": the §12 adapters
|
|
5
|
+
* return a `SpotOrderResult` (rejected / cancelled / pending / partial / filled),
|
|
6
|
+
* and the legacy Hyperliquid / Lighter adapters return a venue payload. Every bot /
|
|
7
|
+
* arb spot leg goes through `spotMarketFill()` so the hedge is sized to what
|
|
8
|
+
* actually filled:
|
|
9
|
+
*
|
|
10
|
+
* - rejected / cancelled (nothing filled) → throws ORDER_REJECTED
|
|
11
|
+
* - pending / open / fill size not reported → throws ORDER_PENDING (do NOT retry)
|
|
12
|
+
* - the order call itself throws → a PerpError with a definite code
|
|
13
|
+
* (isDefiniteOrderFailure: validation / policy / explicit venue rejection) is
|
|
14
|
+
* rethrown as a definite failure; ANYTHING else (non-PerpError, EXCHANGE_UNREACHABLE,
|
|
15
|
+
* TIMEOUT, EXCHANGE_ERROR, RATE_LIMITED, UNKNOWN, …) becomes ORDER_PENDING — the
|
|
16
|
+
* request may have reached the venue, so callers quarantine instead of retrying.
|
|
17
|
+
* - partial → returns the filled size
|
|
18
|
+
* - filled → returns the filled size
|
|
19
|
+
*
|
|
20
|
+
* Legacy results:
|
|
21
|
+
* - Hyperliquid: `response.data.statuses[0].filled.totalSz` (resting → pending).
|
|
22
|
+
* - Lighter: the adapter reads the fill from Lighter's own /tx event_info (taker
|
|
23
|
+
* order "filled") or, for any other status, the order's accountOrders row
|
|
24
|
+
* (filled_base_amount), and returns a final SpotOrderResult that iocOutcomeOf() reports
|
|
25
|
+
* (venueFinalSpotFill below). A SpotOrderResult marked TX_EVENT_FILL_SOURCE that is not
|
|
26
|
+
* routed through iocOutcomeOf is cross-checked as follows; because
|
|
27
|
+
* the is/rs semantics are not venue-documented, that fill must be backed by a fresh
|
|
28
|
+
* base-balance delta of ≥ 80% (else ORDER_PENDING). Only when the venue gives no fill
|
|
29
|
+
* report does it return the raw tx hash; the fill is then MEASURED as the base-token
|
|
30
|
+
* balance delta around the order using UNCACHED reads (readSpotBalancesFresh). A
|
|
31
|
+
* zero delta is ORDER_PENDING (the IOC may still be unconfirmed), never "nothing
|
|
32
|
+
* filled"; a failed read is never zero.
|
|
33
|
+
*
|
|
34
|
+
* Venue-final IOC outcome (design §13.10 #4): when the spot adapter is IocOutcomeCapable
|
|
35
|
+
* and iocOutcomeOf(result) reports the order's FINAL filled size, that report is used in
|
|
36
|
+
* preference to the balance delta, cross-checked against a fresh base-balance delta:
|
|
37
|
+
* - final 0 and the balance did not move (≤ one lot) → ORDER_REJECTED (definite);
|
|
38
|
+
* the balance DID move → the measured delta is used (discrepancy logged to stderr);
|
|
39
|
+
* - final > 0 → filledSize = the reported size when the delta is ≥ 80% of it (base-token
|
|
40
|
+
* fees), else ORDER_PENDING; an unreadable balance → ORDER_PENDING.
|
|
41
|
+
*/
|
|
42
|
+
import { type SpotAdapter, type SpotBalance } from "./spot-interface.js";
|
|
43
|
+
export interface SpotFill {
|
|
44
|
+
status: "filled" | "partial";
|
|
45
|
+
/** Base units actually filled (> 0). */
|
|
46
|
+
filledSize: number;
|
|
47
|
+
avgPrice: number | null;
|
|
48
|
+
orderId: string | null;
|
|
49
|
+
/** The adapter's raw result (for logs). */
|
|
50
|
+
raw: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Interpret a spot order result. `measuredFill` = base balance delta measured around
|
|
54
|
+
* the order (null when not measured); used only for results that carry no fill report.
|
|
55
|
+
*/
|
|
56
|
+
export declare function interpretSpotFill(venue: string, symbol: string, result: unknown, requestedSize: number, measuredFill: number | null): SpotFill;
|
|
57
|
+
/**
|
|
58
|
+
* Uncached spot balances: getSpotBalancesFresh() on adapters whose getSpotBalances()
|
|
59
|
+
* is TTL-cached (SpotFreshBalancesCapable contract), getSpotBalances() on adapters that
|
|
60
|
+
* read live. Use for fill measurement and post-trade verification. Throws on failure.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readSpotBalancesFresh(spot: SpotAdapter): Promise<SpotBalance[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Place a spot market order and return what filled (see header). The base balance is
|
|
65
|
+
* read before the order so venues without a fill report can be measured; a failure of
|
|
66
|
+
* that read aborts BEFORE anything is placed. After the order, errors never throw a
|
|
67
|
+
* retryable error: an unreadable outcome is ORDER_PENDING.
|
|
68
|
+
*/
|
|
69
|
+
export declare function spotMarketFill(spot: SpotAdapter, symbol: string, side: "buy" | "sell", size: string, stepSize?: string | null): Promise<SpotFill>;
|
|
70
|
+
/** Decimal string for a filled size (no exponent), floored to `decimals`. */
|
|
71
|
+
export declare function fillSizeString(filled: number, decimals: number): string;
|
|
72
|
+
/** Decimal places of a plain decimal size string ("0.0100" → 4). */
|
|
73
|
+
export declare function sizeDecimalsOf(size: string): number;
|
|
74
|
+
/**
|
|
75
|
+
* Spot lot step (10^-sizeDecimals) from the venue's spot market info. `symbol` is "BASE/QUOTE"
|
|
76
|
+
* or a bare base; a bare base whose markets disagree on the lot, or an unknown market, throws
|
|
77
|
+
* (Rule #2: never size off the typed decimals).
|
|
78
|
+
*/
|
|
79
|
+
export declare function spotLotStepOf(spot: SpotAdapter, symbol: string): Promise<string>;
|