perp-cli 0.14.3 → 0.16.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.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +138 -405
- package/README.zh.md +243 -0
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +85 -15
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +12 -5
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
- package/dist/bot/strategies/funding-arb-utils.js +53 -20
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +3 -3
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +126 -6
- package/dist/commands/account.js +168 -24
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +23 -81
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +175 -97
- package/dist/commands/arb.d.ts +20 -0
- package/dist/commands/arb.js +107 -163
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +395 -100
- package/dist/commands/options.d.ts +14 -0
- package/dist/commands/options.js +151 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +455 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +83 -55
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +67 -62
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +3 -3
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +35 -3
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +16 -12
- package/dist/exchanges/arcus/adapter.js +45 -18
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +30 -10
- package/dist/exchanges/aster.js +153 -53
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +8 -2
- package/dist/exchanges/canonical-symbols.js +46 -2
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +73 -0
- package/dist/exchanges/derive/options.js +379 -0
- package/dist/exchanges/extended/adapter.d.ts +96 -0
- package/dist/exchanges/extended/adapter.js +241 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +133 -0
- package/dist/exchanges/extended/public.js +436 -0
- package/dist/exchanges/grvt/adapter.d.ts +100 -0
- package/dist/exchanges/grvt/adapter.js +283 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
- package/dist/exchanges/hyperliquid-spot.js +24 -16
- package/dist/exchanges/hyperliquid.d.ts +199 -22
- package/dist/exchanges/hyperliquid.js +550 -202
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +157 -13
- package/dist/exchanges/interface.js +47 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +67 -12
- package/dist/exchanges/lighter-public.js +250 -82
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +10 -5
- package/dist/exchanges/lighter-spot.js +105 -97
- package/dist/exchanges/lighter.d.ts +80 -21
- package/dist/exchanges/lighter.js +312 -105
- package/dist/exchanges/ondo/adapter.d.ts +38 -13
- package/dist/exchanges/ondo/adapter.js +86 -27
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +35 -3
- package/dist/exchanges/ondo/public.js +75 -32
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +8 -2
- package/dist/exchanges/pacifica-spot.js +26 -18
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +130 -0
- package/dist/exchanges/pacifica-vaults.js +277 -0
- package/dist/exchanges/pacifica.d.ts +108 -10
- package/dist/exchanges/pacifica.js +428 -171
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +69 -13
- package/dist/exchanges/qfex/adapter.js +102 -17
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +65 -12
- package/dist/exchanges/qfex/public.js +131 -25
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +160 -0
- package/dist/exchanges/risex/adapter.js +842 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +8 -8
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +61 -0
- package/dist/exchanges/txflow/adapter.js +150 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +155 -0
- package/dist/exchanges/txflow/public.d.ts +87 -0
- package/dist/exchanges/txflow/public.js +355 -0
- package/dist/exchanges/unknown-symbol.d.ts +34 -0
- package/dist/exchanges/unknown-symbol.js +56 -0
- package/dist/exchanges/variational/adapter.d.ts +67 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +52 -10
- package/dist/exchanges/venue-types.js +82 -11
- package/dist/exchanges/venue.d.ts +150 -11
- package/dist/exchanges/venue.js +126 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +19 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +20 -8
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +18 -6
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +15 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +131 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +6 -4
- package/dist/funding/legacy-public.js +42 -41
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +95 -6
- package/dist/funding/rates.js +99 -19
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +64 -0
- package/dist/mcp-server.js +211 -68
- package/dist/pacifica/client.d.ts +69 -5
- package/dist/pacifica/client.js +181 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +36 -63
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +980 -0
- package/dist/ws/index.js +4460 -0
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +996 -0
- package/dist/ws/node.js +4554 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +71 -10
- package/skills/perp-cli/SKILL.md +153 -36
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +22 -12
- package/skills/perp-cli/references/commands.md +143 -17
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +523 -0
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -5,12 +5,12 @@ import { tradingV2Of } from "../exchanges/legacy-trading-v2.js";
|
|
|
5
5
|
import { invalidateCache } from "../cache.js";
|
|
6
6
|
import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
-
import { formatUsd, printJson, jsonOk, jsonError, makeTable,
|
|
8
|
+
import { formatUsd, printJson, jsonOk, jsonError, makeTable, symbolMatch } from "../utils.js";
|
|
9
9
|
import { computeExecutableSize } from "../liquidity.js";
|
|
10
10
|
import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js";
|
|
11
|
-
import { toHourlyRate } from "../funding.js";
|
|
11
|
+
import { getFundingHours, toHourlyRate } from "../funding.js";
|
|
12
|
+
import { fetchVenuesRates } from "../funding/rates.js";
|
|
12
13
|
import { toHourlyRateByBasis } from "../funding/normalize.js";
|
|
13
|
-
import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
|
|
14
14
|
import { fetchAllDexAssetsDetailed, findDexArbPairs, scanDexArb } from "../dex-asset-map.js";
|
|
15
15
|
import { logExecution, readExecutionLog } from "../execution-log.js";
|
|
16
16
|
import { smartOrder } from "../smart-order.js";
|
|
@@ -18,9 +18,23 @@ import { aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk
|
|
|
18
18
|
import { checkChainMargins, marginEntryBlocks, computeAutoSize, } from "../cross-chain-margin.js";
|
|
19
19
|
import { loadArbState, addPosition as persistAddPosition, removePosition as persistRemovePosition, updatePosition as persistUpdatePosition, createInitialState, createDaemonStateStore, } from "../arb-state.js";
|
|
20
20
|
import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
|
|
21
|
+
/** The venue symbol a snapshot / position leg trades on `exchange` (the canonical key when the venue uses it). */
|
|
22
|
+
export function legSymbol(row, exchange) {
|
|
23
|
+
return row.venueSymbols?.[exchange] ?? row.symbol;
|
|
24
|
+
}
|
|
25
|
+
/** longSymbol / shortSymbol of a new position — only when a leg's venue symbol differs from the key. */
|
|
26
|
+
function legSymbolsOf(snap, longExchange, shortExchange) {
|
|
27
|
+
const l = legSymbol(snap, longExchange), sh = legSymbol(snap, shortExchange);
|
|
28
|
+
return l === snap.symbol && sh === snap.symbol ? {} : { longSymbol: l, shortSymbol: sh };
|
|
29
|
+
}
|
|
30
|
+
/** --symbols matches a row by its canonical key or any venue's own symbol (kPEPE or 1000PEPE). */
|
|
31
|
+
export function matchesSymbolFilter(row, filter) {
|
|
32
|
+
const want = new Set(filter.map(s => s.toUpperCase()));
|
|
33
|
+
return want.has(row.symbol.toUpperCase()) || Object.values(row.venueSymbols ?? {}).some(v => want.has(v.toUpperCase()));
|
|
34
|
+
}
|
|
21
35
|
// ── Fee-Adjusted Net Spread Calculation ──
|
|
22
36
|
import { getTakerFee } from "../constants.js";
|
|
23
|
-
import { assertFundingArbVenue, getVenue, listVenuesWithRole,
|
|
37
|
+
import { assertFundingArbVenue, getVenue, listVenuesWithRole, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
|
|
24
38
|
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
25
39
|
import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
|
|
26
40
|
import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
|
|
@@ -380,67 +394,60 @@ export function dexCloseBlockedBy(pos, failedDexes) {
|
|
|
380
394
|
return null;
|
|
381
395
|
}
|
|
382
396
|
const _warnedArbPriceMismatch = new Set();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const hlRes = settled[1].status === "fulfilled" ? settled[1].value : null;
|
|
401
|
-
const ltDetailsRes = settled[2].status === "fulfilled" ? settled[2].value : null;
|
|
402
|
-
const ltFundingRes = settled[3].status === "fulfilled" ? settled[3].value : null;
|
|
403
|
-
// Both Aster calls are needed: a rate without its per-symbol interval cannot be normalized.
|
|
404
|
-
const astPremiums = settled[4].status === "fulfilled" && settled[5].status === "fulfilled"
|
|
405
|
-
? settled[4].value
|
|
406
|
-
: [];
|
|
407
|
-
const astFundingInfo = settled[5].status === "fulfilled" ? settled[5].value : [];
|
|
397
|
+
/** " (lastSettled)"-style tag of a snapshot leg's rate kind; "" when the venue is not a leg. */
|
|
398
|
+
const kindTag = (s, ex) => { const k = s.rateKinds?.[ex]; return k === undefined ? "" : `(${k ?? "?"})`; };
|
|
399
|
+
export async function fetchFundingSpreads() {
|
|
400
|
+
// The SAME rate rows as `arb scan --rates` (fetchVenuesRates over each venue's public directory):
|
|
401
|
+
// one rate per venue and canonical symbol (unclassified / ambiguous tickers and non-matchable
|
|
402
|
+
// listings have no canonical and never pair), each with its basis and rateKind. A venue that
|
|
403
|
+
// fails is reported, never silently treated as "no spread".
|
|
404
|
+
const venues = ["pacifica", "hyperliquid", "lighter", "aster"].map(id => getVenue(id));
|
|
405
|
+
const { rates: fetched, exchangeErrors } = await fetchVenuesRates(venues, sharedApiNetwork(), false);
|
|
406
|
+
// Never a leg on an unverified rate (row label; the venue filter above already drops them).
|
|
407
|
+
const rates = fetched.filter(r => r.fundingVerified);
|
|
408
|
+
for (const [ex, msg] of Object.entries(exchangeErrors))
|
|
409
|
+
process.stderr.write(`[arb-auto] ${ex}: funding rates unavailable — ${msg}\n`);
|
|
410
|
+
const pacRates = new Map(), pacPrices = new Map();
|
|
411
|
+
const hlRates = new Map(), hlPrices = new Map();
|
|
412
|
+
const ltRates = new Map(), ltPrices = new Map();
|
|
413
|
+
const astRates = new Map(), astPrices = new Map();
|
|
408
414
|
const astHoursBySym = new Map();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const hours = Number(row.fundingIntervalHours);
|
|
412
|
-
if (rawSym.endsWith("USDT") && Number.isFinite(hours) && hours > 0)
|
|
413
|
-
astHoursBySym.set(rawSym.replace(/USDT$/, ""), hours);
|
|
414
|
-
}
|
|
415
|
-
const { rates: pacRates, prices: pacPrices } = parsePacificaRaw(pacRes);
|
|
416
|
-
const { rates: hlRates, prices: hlPrices } = parseHyperliquidMetaRaw(hlRes);
|
|
417
|
-
const { rates: ltRates, prices: ltPrices } = parseLighterRaw(ltDetailsRes, ltFundingRes);
|
|
418
|
-
// Aster: parse premiumIndex.
|
|
419
|
-
// SSOT rule #2: never silently coerce missing markPrice/lastFundingRate to 0
|
|
420
|
-
// — a 0 markPrice corrupts every downstream sizing calculation, and a 0
|
|
421
|
-
// funding rate flips the symbol off the arb radar without explanation.
|
|
422
|
-
// Skip the symbol entirely if either field is missing or non-finite.
|
|
423
|
-
const astRates = new Map();
|
|
424
|
-
const astPrices = new Map();
|
|
415
|
+
const kindsBySym = new Map();
|
|
416
|
+
const venueSymbolsBySym = new Map();
|
|
425
417
|
const astNoInterval = [];
|
|
426
|
-
for (const
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
418
|
+
for (const r of rates) {
|
|
419
|
+
const sym = r.canonical;
|
|
420
|
+
const kinds = kindsBySym.get(sym) ?? {};
|
|
421
|
+
kinds[r.exchange] = r.rateKind;
|
|
422
|
+
kindsBySym.set(sym, kinds);
|
|
423
|
+
const vs = venueSymbolsBySym.get(sym) ?? {};
|
|
424
|
+
vs[r.exchange] = r.symbol; // the venue's own symbol (orders use it)
|
|
425
|
+
venueSymbolsBySym.set(sym, vs);
|
|
426
|
+
if (r.exchange === "aster") {
|
|
427
|
+
if (r.basis.kind !== "perHours") {
|
|
428
|
+
astNoInterval.push(r.symbol);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
astRates.set(sym, r.fundingRate);
|
|
432
|
+
astPrices.set(sym, r.markPrice);
|
|
433
|
+
astHoursBySym.set(sym, r.basis.hours);
|
|
436
434
|
continue;
|
|
437
435
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
// The snapshot keeps each venue's rate in the venue's own basis (snapshotHourly /
|
|
437
|
+
// computeAnnualSpread normalize with it): convert from the row's hourly rate.
|
|
438
|
+
const inVenueBasis = r.hourlyRate * getFundingHours(r.exchange);
|
|
439
|
+
if (r.exchange === "pacifica") {
|
|
440
|
+
pacRates.set(sym, inVenueBasis);
|
|
441
|
+
pacPrices.set(sym, r.markPrice);
|
|
442
|
+
}
|
|
443
|
+
else if (r.exchange === "hyperliquid") {
|
|
444
|
+
hlRates.set(sym, inVenueBasis);
|
|
445
|
+
hlPrices.set(sym, r.markPrice);
|
|
446
|
+
}
|
|
447
|
+
else if (r.exchange === "lighter") {
|
|
448
|
+
ltRates.set(sym, inVenueBasis);
|
|
449
|
+
ltPrices.set(sym, r.markPrice);
|
|
441
450
|
}
|
|
442
|
-
astRates.set(sym, fundingRate);
|
|
443
|
-
astPrices.set(sym, markPrice);
|
|
444
451
|
}
|
|
445
452
|
if (astNoInterval.length > 0) {
|
|
446
453
|
process.stderr.write(`[arb-auto] aster: ${astNoInterval.length} symbol(s) excluded — no fundingIntervalHours in /fapi/v3/fundingInfo: ${astNoInterval.slice(0, 10).join(", ")}${astNoInterval.length > 10 ? ", …" : ""}\n`);
|
|
@@ -528,12 +535,54 @@ async function fetchFundingSpreads() {
|
|
|
528
535
|
ltMarkPrice: ltPrices.get(sym) ?? 0,
|
|
529
536
|
astMarkPrice: astPrices.get(sym) ?? 0,
|
|
530
537
|
astHours: ast !== undefined ? astHours : null,
|
|
538
|
+
rateKinds: Object.fromEntries(available.map(r => [r.exchange, kindsBySym.get(sym)?.[r.exchange] ?? null])),
|
|
539
|
+
venueSymbols: venueSymbolsBySym.get(sym) ?? {},
|
|
531
540
|
});
|
|
532
541
|
}
|
|
533
542
|
return snapshots.sort((a, b) => Math.abs(b.spread) - Math.abs(a.spread));
|
|
534
543
|
}
|
|
535
544
|
// Module-level holder for spot-close handler -- assigned inside registerArbAutoCommands.
|
|
536
545
|
export let handleSpotClose;
|
|
546
|
+
/**
|
|
547
|
+
* `arb auto` flag validation: every numeric flag must be a finite number in its range, `--size` a
|
|
548
|
+
* number or "auto", `--settle-strategy` one of block | aggressive | off. INVALID_PARAMS otherwise.
|
|
549
|
+
*/
|
|
550
|
+
export function assertArbAutoFlags(opts) {
|
|
551
|
+
const num = (flag, key, min, opt = {}) => {
|
|
552
|
+
const raw = opts[key];
|
|
553
|
+
if (raw === undefined)
|
|
554
|
+
return;
|
|
555
|
+
const text = String(raw).trim();
|
|
556
|
+
const v = Number(text);
|
|
557
|
+
const ok = text !== "" && Number.isFinite(v) && (opt.exclusiveMin ? v > min : v >= min) && (!opt.integer || Number.isInteger(v));
|
|
558
|
+
if (!ok) {
|
|
559
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be ${opt.integer ? "a whole number" : "a number"} ${opt.exclusiveMin ? ">" : ">="} ${min}, got "${text}"`, {
|
|
560
|
+
remediation: `Pass e.g. ${flag} ${opt.exclusiveMin ? min + 1 : Math.max(min, 1)}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
num("--min-spread", "minSpread", 0);
|
|
565
|
+
num("--close-spread", "closeSpread", 0);
|
|
566
|
+
if (opts.size !== undefined && String(opts.size).toLowerCase() !== "auto")
|
|
567
|
+
num("--size", "size", 0, { exclusiveMin: true });
|
|
568
|
+
num("--min-size", "minSize", 0);
|
|
569
|
+
num("--max-positions", "maxPositions", 1, { integer: true });
|
|
570
|
+
num("--interval", "interval", 0, { integer: true });
|
|
571
|
+
num("--hold-days", "holdDays", 0, { exclusiveMin: true });
|
|
572
|
+
num("--bridge-cost", "bridgeCost", 0);
|
|
573
|
+
num("--min-margin", "minMargin", 0);
|
|
574
|
+
num("--max-basis", "maxBasis", 0, { exclusiveMin: true });
|
|
575
|
+
num("--cooldown", "cooldown", 0);
|
|
576
|
+
num("--min-oi", "minOi", 0);
|
|
577
|
+
const strat = opts.settleStrategy;
|
|
578
|
+
if (strat !== undefined && !["block", "aggressive", "off"].includes(String(strat))) {
|
|
579
|
+
throw new PerpError("INVALID_PARAMS", `--settle-strategy must be block, aggressive or off, got "${String(strat)}"`, { remediation: "Pass --settle-strategy block (default)" });
|
|
580
|
+
}
|
|
581
|
+
const grade = opts.minGrade;
|
|
582
|
+
if (grade !== undefined && !["A", "B", "C", "D"].includes(String(grade).toUpperCase())) {
|
|
583
|
+
throw new PerpError("INVALID_PARAMS", `--min-grade must be A, B, C or D, got "${String(grade)}"`, { remediation: "Pass --min-grade C (default)" });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
537
586
|
export function registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex) {
|
|
538
587
|
const arb = program.commands.find(c => c.name() === "arb");
|
|
539
588
|
if (!arb)
|
|
@@ -567,6 +616,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
567
616
|
.option("--min-oi <usd>", "Min OI (USD) to enter (hip3 mode)", "50000")
|
|
568
617
|
.option("--min-grade <grade>", "Min viability grade: A, B, C, D (hip3 mode)", "C")
|
|
569
618
|
.action(async (opts) => {
|
|
619
|
+
// Numeric flags are checked up front (also before --background starts a job): a value that is
|
|
620
|
+
// not a number ("--cooldown abc" → NaN) would otherwise silently disable the check it feeds.
|
|
621
|
+
assertArbAutoFlags(opts);
|
|
570
622
|
// Route to HIP-3 dex-auto logic if --hip3 flag is set
|
|
571
623
|
if (opts.hip3) {
|
|
572
624
|
return handleDexAuto({
|
|
@@ -647,6 +699,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
647
699
|
const marginUnknownNotified = new Set();
|
|
648
700
|
// Track close times per symbol to prevent close→re-entry loops
|
|
649
701
|
const closeCooldowns = new Map(); // symbol → close timestamp ms
|
|
702
|
+
// --symbols entries already reported as resolving to another canonical symbol (logged once).
|
|
703
|
+
const aliasNoted = new Set();
|
|
650
704
|
// Consecutive definite close failures per symbol (quarantined at MAX_PAIR_CLOSE_FAILURES)
|
|
651
705
|
const closeFailures = new Map();
|
|
652
706
|
// -- State Persistence: Initialize or recover --
|
|
@@ -687,7 +741,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
687
741
|
(await getAdapterForExchange(p.longExchange)).getPositions(),
|
|
688
742
|
(await getAdapterForExchange(p.shortExchange)).getPositions(),
|
|
689
743
|
]);
|
|
690
|
-
const has = (list, side) => list.some(x => symbolMatch(x.symbol, p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
|
|
744
|
+
const has = (list, side) => list.some(x => symbolMatch(x.symbol, (side === "long" ? p.longSymbol : p.shortSymbol) ?? p.symbol) && x.side === side && Math.abs(Number(x.size)) > 0);
|
|
691
745
|
if (has(longs, "long") && has(shorts, "short")) {
|
|
692
746
|
staleSymbols.delete(p.symbol);
|
|
693
747
|
return true;
|
|
@@ -722,6 +776,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
722
776
|
entryMarkPrice: persisted.entryLongPrice,
|
|
723
777
|
accumulatedFundingUsd: persisted.accumulatedFunding,
|
|
724
778
|
lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
|
|
779
|
+
longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
|
|
725
780
|
});
|
|
726
781
|
}
|
|
727
782
|
daemonState.lastStartTime = new Date().toISOString();
|
|
@@ -793,6 +848,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
793
848
|
entryMarkPrice: persisted.entryLongPrice,
|
|
794
849
|
accumulatedFundingUsd: persisted.accumulatedFunding,
|
|
795
850
|
lastCheckTime: new Date(persisted.lastCheckTime).getTime(),
|
|
851
|
+
longSymbol: persisted.longSymbol, shortSymbol: persisted.shortSymbol,
|
|
796
852
|
});
|
|
797
853
|
}
|
|
798
854
|
}
|
|
@@ -813,7 +869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
813
869
|
try {
|
|
814
870
|
const spreads = await fetchFundingSpreads();
|
|
815
871
|
const filtered = filterSymbols
|
|
816
|
-
? spreads.filter(s =>
|
|
872
|
+
? spreads.filter(s => matchesSymbolFilter(s, filterSymbols))
|
|
817
873
|
: spreads;
|
|
818
874
|
const now = new Date().toLocaleTimeString();
|
|
819
875
|
// Check for close conditions on open positions
|
|
@@ -821,13 +877,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
821
877
|
if (stop.requested())
|
|
822
878
|
break; // stopping: no new close sequence
|
|
823
879
|
const pos = openPositions[i];
|
|
824
|
-
// Quarantined
|
|
880
|
+
// Quarantined: hands off until a human clears it.
|
|
825
881
|
if (isQuarantined(pos.symbol, (m) => console.log(chalk.red(m))))
|
|
826
882
|
continue;
|
|
827
883
|
// Per-position isolation: one position's failing exit check (e.g. an
|
|
828
884
|
// unknown funding basis in snapshotHourly) must not skip the others.
|
|
829
885
|
try {
|
|
830
|
-
|
|
886
|
+
// By canonical key; a position opened before 0.15.1 is keyed by its (shared) venue symbol.
|
|
887
|
+
const current = filtered.find(s => s.symbol === pos.symbol
|
|
888
|
+
|| (legSymbol(s, pos.longExchange) === pos.symbol && legSymbol(s, pos.shortExchange) === pos.symbol));
|
|
831
889
|
if (!current)
|
|
832
890
|
continue;
|
|
833
891
|
const currentSpread = Math.abs(current.spread);
|
|
@@ -877,15 +935,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
877
935
|
try {
|
|
878
936
|
const longAdapter = await getAdapterForExchange(pos.longExchange);
|
|
879
937
|
const shortAdapter = await getAdapterForExchange(pos.shortExchange);
|
|
880
|
-
// Sequential, reduce-only, verified
|
|
938
|
+
// Sequential, reduce-only, verified: long first, then
|
|
881
939
|
// short. A definite long-close failure changes nothing → retried next
|
|
882
940
|
// cycle, quarantined after MAX_PAIR_CLOSE_FAILURES in a row. An unknown
|
|
883
941
|
// outcome, a partial close or an unhedged leg quarantines (no retry).
|
|
884
942
|
const sizeStr = String(pos.size);
|
|
885
943
|
const closeRes = await closePerpPairVerified({
|
|
886
944
|
source: "arb-auto", sym: pos.symbol,
|
|
887
|
-
longAdapter, longExch: pos.longExchange, longSym: pos.symbol, longSize: sizeStr,
|
|
888
|
-
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.symbol, shortSize: sizeStr, smart: false,
|
|
945
|
+
longAdapter, longExch: pos.longExchange, longSym: pos.longSymbol ?? pos.symbol, longSize: sizeStr,
|
|
946
|
+
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.shortSymbol ?? pos.symbol, shortSize: sizeStr, smart: false,
|
|
889
947
|
log: (m) => console.log(chalk.red(m)),
|
|
890
948
|
});
|
|
891
949
|
if (closeRes.status === "closed") {
|
|
@@ -1098,8 +1156,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1098
1156
|
}
|
|
1099
1157
|
};
|
|
1100
1158
|
for (const sym of filterSymbols ?? []) {
|
|
1101
|
-
|
|
1159
|
+
// A --symbols entry may be a venue form (kPEPE): it monitors the canonical row it resolves to.
|
|
1160
|
+
const hit = filtered.find(x => matchesSymbolFilter(x, [sym]));
|
|
1161
|
+
if (hit === undefined)
|
|
1102
1162
|
console.log(chalk.gray(` ${now} SKIP ${sym}: not in this cycle's funding scan (no rate on two compared venues)`));
|
|
1163
|
+
else if (hit.symbol !== sym && !aliasNoted.has(sym)) {
|
|
1164
|
+
aliasNoted.add(sym);
|
|
1165
|
+
const forms = Object.entries(hit.venueSymbols ?? {}).map(([v, vs]) => `${v} ${vs}`).join(" / ");
|
|
1166
|
+
console.log(chalk.gray(` ${now} MONITOR ${sym} → ${hit.symbol} (canonical${forms !== "" ? `; ${forms}` : ""})`));
|
|
1167
|
+
}
|
|
1103
1168
|
}
|
|
1104
1169
|
if (openPositions.length >= maxPositions) {
|
|
1105
1170
|
console.log(chalk.gray(` ${now} NO ENTRY: max positions reached (${openPositions.length}/${maxPositions}: ${openPositions.map(p => `${p.symbol} ${p.longExchange}/${p.shortExchange}`).join(", ")})`));
|
|
@@ -1114,7 +1179,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1114
1179
|
skip(snap.symbol, "already open");
|
|
1115
1180
|
continue;
|
|
1116
1181
|
}
|
|
1117
|
-
// Global quarantine
|
|
1182
|
+
// Global quarantine: unknown / unhedged outcome on this symbol somewhere.
|
|
1118
1183
|
if (isQuarantined(snap.symbol, (m) => console.log(chalk.red(m)))) {
|
|
1119
1184
|
skip(snap.symbol, "quarantined");
|
|
1120
1185
|
continue;
|
|
@@ -1195,7 +1260,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1195
1260
|
try {
|
|
1196
1261
|
const longAdapter = await getAdapterForExchange(longExchange);
|
|
1197
1262
|
const shortAdapter = await getAdapterForExchange(shortExchange);
|
|
1198
|
-
actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, snap
|
|
1263
|
+
actualSizeUsd = await computeAutoSize(longAdapter, shortAdapter, legSymbol(snap, longExchange), 0.3, legSymbol(snap, shortExchange));
|
|
1199
1264
|
if (actualSizeUsd <= 0) {
|
|
1200
1265
|
console.log(chalk.gray(` ${now} SKIP ${snap.symbol}: auto-size returned $0 (insufficient depth/margin)`));
|
|
1201
1266
|
continue;
|
|
@@ -1234,12 +1299,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1234
1299
|
try {
|
|
1235
1300
|
const longAdapter = await getAdapterForExchange(longExchange);
|
|
1236
1301
|
const shortAdapter = await getAdapterForExchange(shortExchange);
|
|
1237
|
-
// Sequential verified legs
|
|
1302
|
+
// Sequential verified legs: long, then short sized to the
|
|
1238
1303
|
// long's verified fill; reduce-only rollback / trim; unknown → quarantine.
|
|
1239
1304
|
legsStarted = true;
|
|
1240
1305
|
const res = await openPerpPairVerified({
|
|
1241
1306
|
source: "arb-auto", sym: snap.symbol,
|
|
1242
|
-
longAdapter, longExch: longExchange, longSym: snap
|
|
1307
|
+
longAdapter, longExch: longExchange, longSym: legSymbol(snap, longExchange), shortAdapter, shortExch: shortExchange, shortSym: legSymbol(snap, shortExchange),
|
|
1243
1308
|
size: sizeInAsset, smart: false, log: (m) => console.log(chalk.red(m)),
|
|
1244
1309
|
});
|
|
1245
1310
|
legsStarted = false; // outcome known (opened / not opened / quarantined)
|
|
@@ -1268,6 +1333,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1268
1333
|
entryShortPrice: snap.markPrice,
|
|
1269
1334
|
accumulatedFunding: 0,
|
|
1270
1335
|
lastCheckTime: entryTime,
|
|
1336
|
+
...legSymbolsOf(snap, longExchange, shortExchange),
|
|
1271
1337
|
});
|
|
1272
1338
|
}
|
|
1273
1339
|
catch (e) {
|
|
@@ -1291,6 +1357,9 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1291
1357
|
});
|
|
1292
1358
|
console.error(chalk.red(` ${now} ENTRY FAILED ${snap.symbol}: ${res.reason}${res.rolledBack !== null ? ` — rolled back long ${res.rolledBack}; no open exposure` : " — nothing opened"}`));
|
|
1293
1359
|
donePending();
|
|
1360
|
+
// Cool down like a close: a leg the venue rejects (unknown symbol, lot, margin)
|
|
1361
|
+
// is not re-sent — with its fees and rollback — every cycle.
|
|
1362
|
+
closeCooldowns.set(snap.symbol, Date.now());
|
|
1294
1363
|
continue;
|
|
1295
1364
|
}
|
|
1296
1365
|
else {
|
|
@@ -1341,6 +1410,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1341
1410
|
entryMarkPrice: snap.markPrice,
|
|
1342
1411
|
accumulatedFundingUsd: 0,
|
|
1343
1412
|
lastCheckTime: Date.now(),
|
|
1413
|
+
...legSymbolsOf(snap, longExchange, shortExchange),
|
|
1344
1414
|
});
|
|
1345
1415
|
// Dry-run: record the simulated entry in the in-memory state only (never the real file).
|
|
1346
1416
|
if (dryRun) {
|
|
@@ -1445,7 +1515,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1445
1515
|
.option("--duration <minutes>", "Track duration in minutes (with --track)", "10")
|
|
1446
1516
|
.option("--beep", "Beep on large gaps (with --gaps)")
|
|
1447
1517
|
.option("--check-liquidity", "Check orderbook depth in live mode (slower)")
|
|
1448
|
-
.option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex) — display only, never ranked for trading")
|
|
1518
|
+
.option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex; txflow — unofficial API; variational; grvt — indicative rate; extended) — display only, never ranked for trading")
|
|
1449
1519
|
.action(async (opts) => {
|
|
1450
1520
|
// ── Price gap modes ──
|
|
1451
1521
|
if (opts.gaps || opts.track || opts.alert) {
|
|
@@ -1594,7 +1664,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1594
1664
|
const topCheck = filtered.slice(0, 5);
|
|
1595
1665
|
await Promise.allSettled(topCheck.map(async (s) => {
|
|
1596
1666
|
try {
|
|
1597
|
-
const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(s
|
|
1667
|
+
const [hlOB, ltOB] = await Promise.all([fetchHLOrderbook(legSymbol(s, "hyperliquid")), fetchLighterOrderbook(legSymbol(s, "lighter"))]);
|
|
1598
1668
|
const hlDepth = hlOB.reduce((sum, l) => sum + l[0] * l[1], 0);
|
|
1599
1669
|
const ltDepth = ltOB.reduce((sum, l) => sum + l[0] * l[1], 0);
|
|
1600
1670
|
const hlBest = hlOB[0]?.[0] ?? 0;
|
|
@@ -1613,12 +1683,13 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1613
1683
|
const grossColor = grossSpread >= 50 ? chalk.green.bold : grossSpread >= 30 ? chalk.green : chalk.yellow;
|
|
1614
1684
|
const netColor = netSpread >= 20 ? chalk.green : netSpread >= 0 ? chalk.yellow : chalk.red;
|
|
1615
1685
|
const rates = [];
|
|
1686
|
+
// Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
|
|
1616
1687
|
if (s.pacRate)
|
|
1617
|
-
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}
|
|
1688
|
+
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
|
|
1618
1689
|
if (s.hlRate)
|
|
1619
|
-
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}
|
|
1690
|
+
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
|
|
1620
1691
|
if (s.ltRate)
|
|
1621
|
-
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}
|
|
1692
|
+
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
|
|
1622
1693
|
let liqInfo = "";
|
|
1623
1694
|
const ld = liqData.get(s.symbol);
|
|
1624
1695
|
if (ld)
|
|
@@ -1682,8 +1753,10 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1682
1753
|
const netSpread = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
|
|
1683
1754
|
return {
|
|
1684
1755
|
mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
|
|
1685
|
-
|
|
1686
|
-
|
|
1756
|
+
// grossSpread / netSpread: the names every other arb scan row uses (same % values);
|
|
1757
|
+
// the *Pct names stay for existing consumers.
|
|
1758
|
+
spotExchanges: s.spotExchanges, grossSpreadPct: s.annualSpreadPct, grossSpread: s.annualSpreadPct,
|
|
1759
|
+
netSpreadPct: netSpread, netSpread, estFeesPct: rtCost,
|
|
1687
1760
|
perpFundingRate: s.perpFundingRate, direction: s.direction,
|
|
1688
1761
|
markPrice: s.bestMarkPrice, estHourlyIncomeUsd: s.estHourlyIncomeUsd,
|
|
1689
1762
|
breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
|
|
@@ -1747,7 +1820,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1747
1820
|
const shortPrice = pFor(s.shortExch);
|
|
1748
1821
|
const mid = (longPrice + shortPrice) / 2;
|
|
1749
1822
|
const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
|
|
1750
|
-
return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, netSpread: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
|
|
1823
|
+
return { mode: "perp-perp", symbol: s.symbol, longExch: s.longExch, shortExch: s.shortExch, markPrice: s.markPrice, grossSpread, grossSpreadPct: grossSpread, netSpread: net, netSpreadPct: net, estFeesPct: rtCost, priceGapPct: Math.round(priceGapPct * 10000) / 10000 };
|
|
1751
1824
|
});
|
|
1752
1825
|
// Merge spot-perp results if "all" mode
|
|
1753
1826
|
const spotPerp = (opts.mode === "all" && spotSpreads) ? spotSpreads.map(s => {
|
|
@@ -1757,8 +1830,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1757
1830
|
const net = computeNetSpread(s.annualSpreadPct, holdDays, rtCost, effectiveBridgeCost, sizeUsd);
|
|
1758
1831
|
return {
|
|
1759
1832
|
mode: "spot-perp", symbol: s.symbol, perpExchange: s.perpExchange,
|
|
1760
|
-
spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct,
|
|
1761
|
-
netSpread: net, estFeesPct: rtCost,
|
|
1833
|
+
spotExchanges: s.spotExchanges, grossSpread: s.annualSpreadPct, grossSpreadPct: s.annualSpreadPct,
|
|
1834
|
+
netSpread: net, netSpreadPct: net, estFeesPct: rtCost,
|
|
1762
1835
|
perpFundingRate: s.perpFundingRate, direction: s.direction,
|
|
1763
1836
|
markPrice: s.bestMarkPrice, breakEvenDays: rtCost > 0 ? Math.ceil(rtCost / (s.annualSpreadPct / 365)) : 0,
|
|
1764
1837
|
};
|
|
@@ -1787,14 +1860,15 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1787
1860
|
const priceGapPct = mid > 0 ? Math.abs(longPrice - shortPrice) / mid * 100 : 0;
|
|
1788
1861
|
const gapColor = priceGapPct >= 0.5 ? chalk.red : priceGapPct >= 0.1 ? chalk.yellow : chalk.gray;
|
|
1789
1862
|
const rates = [];
|
|
1863
|
+
// Each leg names which rate it is (lastSettled / accruing / predicted; ? = unverified).
|
|
1790
1864
|
if (s.pacRate)
|
|
1791
|
-
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}
|
|
1865
|
+
rates.push(`PAC:${(s.pacRate * 100).toFixed(4)}%${kindTag(s, "pacifica")}`);
|
|
1792
1866
|
if (s.hlRate)
|
|
1793
|
-
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}
|
|
1867
|
+
rates.push(`HL:${(s.hlRate * 100).toFixed(4)}%${kindTag(s, "hyperliquid")}`);
|
|
1794
1868
|
if (s.ltRate)
|
|
1795
|
-
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}
|
|
1869
|
+
rates.push(`LT:${(s.ltRate * 100).toFixed(4)}%${kindTag(s, "lighter")}`);
|
|
1796
1870
|
if (s.astRate)
|
|
1797
|
-
rates.push(`AST:${(s.astRate * 100).toFixed(4)}
|
|
1871
|
+
rates.push(`AST:${(s.astRate * 100).toFixed(4)}%${kindTag(s, "aster")}`);
|
|
1798
1872
|
return [
|
|
1799
1873
|
chalk.white.bold(s.symbol),
|
|
1800
1874
|
grossColor(`${grossSpread.toFixed(1)}%`),
|
|
@@ -1857,6 +1931,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1857
1931
|
for (const ex of [longExch, shortExch]) {
|
|
1858
1932
|
const venue = getVenue(ex);
|
|
1859
1933
|
if (!venue.roles.includes("trading")) {
|
|
1934
|
+
if (venue.gatedRoles !== null)
|
|
1935
|
+
throw roleUnsupported(venue, "trading");
|
|
1860
1936
|
throw unsupported(venue.displayName, "trading", "no trading API — it cannot be an arb leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
|
|
1861
1937
|
}
|
|
1862
1938
|
}
|
|
@@ -1867,7 +1943,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1867
1943
|
console.log(chalk.red(" Long and short exchange must be different."));
|
|
1868
1944
|
return;
|
|
1869
1945
|
}
|
|
1870
|
-
// Global quarantine
|
|
1946
|
+
// Global quarantine: an unknown / unhedged outcome on this symbol
|
|
1871
1947
|
// anywhere blocks new trades until a human clears it.
|
|
1872
1948
|
const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
|
|
1873
1949
|
if (quarantined !== null) {
|
|
@@ -1875,7 +1951,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1875
1951
|
remediation: `${quarantined.remediation} — then: perp strategy quarantine-clear ${sym}`,
|
|
1876
1952
|
});
|
|
1877
1953
|
}
|
|
1878
|
-
// A venue whose funding rate is unverified is never a funding-arb leg
|
|
1954
|
+
// A venue whose funding rate is unverified is never a funding-arb leg.
|
|
1879
1955
|
assertFundingArbVenue(longExch);
|
|
1880
1956
|
assertFundingArbVenue(shortExch);
|
|
1881
1957
|
const longAdapter = await getAdapterForExchange(longExch);
|
|
@@ -1991,7 +2067,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1991
2067
|
}
|
|
1992
2068
|
if (!isJson())
|
|
1993
2069
|
console.log(chalk.gray(` Executing legs sequentially (long, then short sized to the verified long fill)...\n`));
|
|
1994
|
-
// 5. Sequential verified legs
|
|
2070
|
+
// 5. Sequential verified legs. The second leg is sized to the
|
|
1995
2071
|
// first leg's VERIFIED fill, any shortfall is trimmed off the long
|
|
1996
2072
|
// (reduce-only, verified — always, --reconcile is implied), a definite short
|
|
1997
2073
|
// failure rolls the long back reduce-only, and an unknown outcome (incl. a
|
|
@@ -2070,12 +2146,14 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2070
2146
|
// Resolve exchange aliases
|
|
2071
2147
|
spotExch = resolveVenueId(spotExch);
|
|
2072
2148
|
perpExch = resolveVenueId(perpExch);
|
|
2073
|
-
// Validate spot exchange: bot/arb spot scope
|
|
2149
|
+
// Validate spot exchange: bot/arb spot scope — throws CAPABILITY_UNSUPPORTED
|
|
2074
2150
|
// for venues without spot or outside the scope (pacifica shared margin, arcus swaps).
|
|
2075
2151
|
const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
|
|
2076
2152
|
spotExch = spotDef.id;
|
|
2077
2153
|
perpExch = tryResolveVenueId(perpExch) ?? perpExch;
|
|
2078
2154
|
if (!getVenue(perpExch).roles.includes("trading")) {
|
|
2155
|
+
if (getVenue(perpExch).gatedRoles !== null)
|
|
2156
|
+
throw roleUnsupported(getVenue(perpExch), "trading");
|
|
2079
2157
|
throw unsupported(perpExch, "trading", "no trading API — it cannot be the perp leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
|
|
2080
2158
|
}
|
|
2081
2159
|
assertFundingArbVenue(perpExch);
|
|
@@ -2093,7 +2171,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2093
2171
|
return;
|
|
2094
2172
|
console.log(chalk.red(` ${msg}`));
|
|
2095
2173
|
};
|
|
2096
|
-
// Global quarantine
|
|
2174
|
+
// Global quarantine: checked before anything is read or moved.
|
|
2097
2175
|
const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
|
|
2098
2176
|
if (quarantined !== null) {
|
|
2099
2177
|
throw new PerpError("RISK_VIOLATION", `${sym} is quarantined (${quarantined.strategy}: ${quarantined.reason})`, {
|
|
@@ -2273,7 +2351,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2273
2351
|
// 7. Execute both legs. Same exchange shares a nonce → sequential; else parallel.
|
|
2274
2352
|
if (!isJson())
|
|
2275
2353
|
console.log(chalk.gray(` Executing ${sameVenue ? "sequentially" : "both legs simultaneously"}...\n`));
|
|
2276
|
-
// The perp leg goes through placePerpVerified
|
|
2354
|
+
// The perp leg goes through placePerpVerified: a fill is the measured
|
|
2277
2355
|
// position delta; an unknown outcome is ORDER_PENDING (quarantine, never roll back).
|
|
2278
2356
|
let spotResult;
|
|
2279
2357
|
let perpResult;
|
|
@@ -2523,7 +2601,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2523
2601
|
remediation: `Pass both: perp arb close ${sym} --spot-exch <spot venue> --perp-exch <perp venue>`,
|
|
2524
2602
|
});
|
|
2525
2603
|
}
|
|
2526
|
-
// Load adapters (bot/arb spot scope
|
|
2604
|
+
// Load adapters (bot/arb spot scope)
|
|
2527
2605
|
const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
|
|
2528
2606
|
const spotAdapter = await requireSpotAdapter(spotDef.id, await getAdapter(spotDef.id));
|
|
2529
2607
|
const perpAdapter = await getAdapter(perpExch);
|
|
@@ -2551,12 +2629,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2551
2629
|
// Read before anything moves — a failure aborts the close.
|
|
2552
2630
|
const preCloseQuote = await readSpotQuoteAvailableFresh(spotAdapter);
|
|
2553
2631
|
// Same exchange → sequential (nonce collision), different → parallel.
|
|
2554
|
-
// The spot leg's REAL fill decides the outcome
|
|
2632
|
+
// The spot leg's REAL fill decides the outcome: only a confirmed spot sale
|
|
2555
2633
|
// counts as closed; legs are re-matched so no naked short / unhedged spot is left.
|
|
2556
2634
|
const sameVenue = spotDef.id === (tryResolveVenueId(perpExch) ?? perpExch);
|
|
2557
2635
|
const closeDec = sizeDecimalsOf(closeSize);
|
|
2558
|
-
// The perp buy-back and every re-match order go through placePerpVerified
|
|
2559
|
-
//
|
|
2636
|
+
// The perp buy-back and every re-match order go through placePerpVerified:
|
|
2637
|
+
// only a measured position change counts; an unknown outcome stops the close.
|
|
2560
2638
|
let spotResult;
|
|
2561
2639
|
let perpResult;
|
|
2562
2640
|
if (sameVenue) {
|
|
@@ -3073,7 +3151,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
3073
3151
|
(p.long.dex === pos.shortDex && p.short.dex === pos.longDex)));
|
|
3074
3152
|
const currentSpread = current?.annualSpread ?? 0;
|
|
3075
3153
|
if (currentSpread <= closeSpread || !current) {
|
|
3076
|
-
// Quarantined
|
|
3154
|
+
// Quarantined: hands off until a human clears it.
|
|
3077
3155
|
if (isQuarantined(pos.underlying, (m) => console.error(chalk.red(m))))
|
|
3078
3156
|
continue;
|
|
3079
3157
|
const reason = !current ? "pair disappeared" : `spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`;
|
package/dist/commands/arb.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface PriceSnapshot {
|
|
|
13
13
|
expensive: string;
|
|
14
14
|
}
|
|
15
15
|
export declare function fetchAllPrices(): Promise<PriceSnapshot[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Cross-venue price gaps by venue symbol. The same ticker on two venues is not proof of the same
|
|
18
|
+
* instrument (Pacifica SAMSUNG 165.8 USD vs Lighter SAMSUNG 220,597 — a KRW-quoted listing): each
|
|
19
|
+
* symbol's prices go through the funding scan's median price-outlier guard (dropPriceOutliers,
|
|
20
|
+
* MAX_PRICE_DEVIATION_PCT, one warning per symbol / venue) before a gap is computed, so a gap is
|
|
21
|
+
* only reported between prices that agree within the guard.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildGapSnapshots(pacPrices: Map<string, number>, hlPrices: Map<string, number>, ltPrices: Map<string, number>): PriceSnapshot[];
|
|
16
24
|
export declare function formatGapPrice(p: number): string;
|
|
17
25
|
export declare function printGapTable(snapshots: PriceSnapshot[], minGap: number): void;
|
|
18
26
|
export declare function printTrackSummary(symbol: string, samples: {
|
|
@@ -29,6 +37,18 @@ export declare function handleRates(isJson: () => boolean, opts: {
|
|
|
29
37
|
top?: string;
|
|
30
38
|
includeUnverified?: boolean;
|
|
31
39
|
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Cross-venue price basis — a PRICE comparison, independent of funding.
|
|
42
|
+
*
|
|
43
|
+
* Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
|
|
44
|
+
* its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
|
|
45
|
+
* come from each venue's market directory and are grouped by `canonical` symbol with the funding
|
|
46
|
+
* scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
|
|
47
|
+
* never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
|
|
48
|
+
* median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
|
|
49
|
+
* a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
|
|
50
|
+
* A venue that fails is reported on stderr and is absent from the scan, never silently empty.
|
|
51
|
+
*/
|
|
32
52
|
export declare function handleBasisScan(isJson: () => boolean, opts: {
|
|
33
53
|
minBasis: string;
|
|
34
54
|
symbol?: string;
|