perp-cli 0.14.3 → 0.15.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 +152 -13
- 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/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- 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/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-utils.js +5 -5
- 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.js +2 -2
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- 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 +125 -5
- 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 +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- 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.js +167 -35
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- 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 +63 -58
- 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/dashboard/server.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- 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/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 +4 -4
- package/dist/exchanges/aster.js +31 -13
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -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 +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -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-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -12
- package/dist/exchanges/hyperliquid.js +80 -34
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +94 -6
- package/dist/exchanges/interface.js +16 -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 +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- 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 +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- 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/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 +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- 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 +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- 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 +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- 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 +2 -2
- 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 +148 -0
- package/dist/exchanges/risex/adapter.js +796 -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 +7 -7
- 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 +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -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 +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -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 +10 -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 +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- 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 +6 -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 +116 -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 +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- 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/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -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/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 +1 -1
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +12 -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/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -20,7 +20,7 @@ import { loadArbState, addPosition as persistAddPosition, removePosition as pers
|
|
|
20
20
|
import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
|
|
21
21
|
// ── Fee-Adjusted Net Spread Calculation ──
|
|
22
22
|
import { getTakerFee } from "../constants.js";
|
|
23
|
-
import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, tryResolveVenueId } from "../exchanges/venue.js";
|
|
23
|
+
import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
|
|
24
24
|
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
25
25
|
import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
|
|
26
26
|
import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
|
|
@@ -821,7 +821,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
821
821
|
if (stop.requested())
|
|
822
822
|
break; // stopping: no new close sequence
|
|
823
823
|
const pos = openPositions[i];
|
|
824
|
-
// Quarantined
|
|
824
|
+
// Quarantined: hands off until a human clears it.
|
|
825
825
|
if (isQuarantined(pos.symbol, (m) => console.log(chalk.red(m))))
|
|
826
826
|
continue;
|
|
827
827
|
// Per-position isolation: one position's failing exit check (e.g. an
|
|
@@ -877,7 +877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
877
877
|
try {
|
|
878
878
|
const longAdapter = await getAdapterForExchange(pos.longExchange);
|
|
879
879
|
const shortAdapter = await getAdapterForExchange(pos.shortExchange);
|
|
880
|
-
// Sequential, reduce-only, verified
|
|
880
|
+
// Sequential, reduce-only, verified: long first, then
|
|
881
881
|
// short. A definite long-close failure changes nothing → retried next
|
|
882
882
|
// cycle, quarantined after MAX_PAIR_CLOSE_FAILURES in a row. An unknown
|
|
883
883
|
// outcome, a partial close or an unhedged leg quarantines (no retry).
|
|
@@ -1114,7 +1114,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1114
1114
|
skip(snap.symbol, "already open");
|
|
1115
1115
|
continue;
|
|
1116
1116
|
}
|
|
1117
|
-
// Global quarantine
|
|
1117
|
+
// Global quarantine: unknown / unhedged outcome on this symbol somewhere.
|
|
1118
1118
|
if (isQuarantined(snap.symbol, (m) => console.log(chalk.red(m)))) {
|
|
1119
1119
|
skip(snap.symbol, "quarantined");
|
|
1120
1120
|
continue;
|
|
@@ -1234,7 +1234,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1234
1234
|
try {
|
|
1235
1235
|
const longAdapter = await getAdapterForExchange(longExchange);
|
|
1236
1236
|
const shortAdapter = await getAdapterForExchange(shortExchange);
|
|
1237
|
-
// Sequential verified legs
|
|
1237
|
+
// Sequential verified legs: long, then short sized to the
|
|
1238
1238
|
// long's verified fill; reduce-only rollback / trim; unknown → quarantine.
|
|
1239
1239
|
legsStarted = true;
|
|
1240
1240
|
const res = await openPerpPairVerified({
|
|
@@ -1445,7 +1445,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1445
1445
|
.option("--duration <minutes>", "Track duration in minutes (with --track)", "10")
|
|
1446
1446
|
.option("--beep", "Beep on large gaps (with --gaps)")
|
|
1447
1447
|
.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")
|
|
1448
|
+
.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
1449
|
.action(async (opts) => {
|
|
1450
1450
|
// ── Price gap modes ──
|
|
1451
1451
|
if (opts.gaps || opts.track || opts.alert) {
|
|
@@ -1857,6 +1857,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1857
1857
|
for (const ex of [longExch, shortExch]) {
|
|
1858
1858
|
const venue = getVenue(ex);
|
|
1859
1859
|
if (!venue.roles.includes("trading")) {
|
|
1860
|
+
if (venue.gatedRoles !== null)
|
|
1861
|
+
throw roleUnsupported(venue, "trading");
|
|
1860
1862
|
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
1863
|
}
|
|
1862
1864
|
}
|
|
@@ -1867,7 +1869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1867
1869
|
console.log(chalk.red(" Long and short exchange must be different."));
|
|
1868
1870
|
return;
|
|
1869
1871
|
}
|
|
1870
|
-
// Global quarantine
|
|
1872
|
+
// Global quarantine: an unknown / unhedged outcome on this symbol
|
|
1871
1873
|
// anywhere blocks new trades until a human clears it.
|
|
1872
1874
|
const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
|
|
1873
1875
|
if (quarantined !== null) {
|
|
@@ -1875,7 +1877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1875
1877
|
remediation: `${quarantined.remediation} — then: perp strategy quarantine-clear ${sym}`,
|
|
1876
1878
|
});
|
|
1877
1879
|
}
|
|
1878
|
-
// A venue whose funding rate is unverified is never a funding-arb leg
|
|
1880
|
+
// A venue whose funding rate is unverified is never a funding-arb leg.
|
|
1879
1881
|
assertFundingArbVenue(longExch);
|
|
1880
1882
|
assertFundingArbVenue(shortExch);
|
|
1881
1883
|
const longAdapter = await getAdapterForExchange(longExch);
|
|
@@ -1991,7 +1993,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
1991
1993
|
}
|
|
1992
1994
|
if (!isJson())
|
|
1993
1995
|
console.log(chalk.gray(` Executing legs sequentially (long, then short sized to the verified long fill)...\n`));
|
|
1994
|
-
// 5. Sequential verified legs
|
|
1996
|
+
// 5. Sequential verified legs. The second leg is sized to the
|
|
1995
1997
|
// first leg's VERIFIED fill, any shortfall is trimmed off the long
|
|
1996
1998
|
// (reduce-only, verified — always, --reconcile is implied), a definite short
|
|
1997
1999
|
// failure rolls the long back reduce-only, and an unknown outcome (incl. a
|
|
@@ -2070,12 +2072,14 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2070
2072
|
// Resolve exchange aliases
|
|
2071
2073
|
spotExch = resolveVenueId(spotExch);
|
|
2072
2074
|
perpExch = resolveVenueId(perpExch);
|
|
2073
|
-
// Validate spot exchange: bot/arb spot scope
|
|
2075
|
+
// Validate spot exchange: bot/arb spot scope — throws CAPABILITY_UNSUPPORTED
|
|
2074
2076
|
// for venues without spot or outside the scope (pacifica shared margin, arcus swaps).
|
|
2075
2077
|
const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
|
|
2076
2078
|
spotExch = spotDef.id;
|
|
2077
2079
|
perpExch = tryResolveVenueId(perpExch) ?? perpExch;
|
|
2078
2080
|
if (!getVenue(perpExch).roles.includes("trading")) {
|
|
2081
|
+
if (getVenue(perpExch).gatedRoles !== null)
|
|
2082
|
+
throw roleUnsupported(getVenue(perpExch), "trading");
|
|
2079
2083
|
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
2084
|
}
|
|
2081
2085
|
assertFundingArbVenue(perpExch);
|
|
@@ -2093,7 +2097,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2093
2097
|
return;
|
|
2094
2098
|
console.log(chalk.red(` ${msg}`));
|
|
2095
2099
|
};
|
|
2096
|
-
// Global quarantine
|
|
2100
|
+
// Global quarantine: checked before anything is read or moved.
|
|
2097
2101
|
const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
|
|
2098
2102
|
if (quarantined !== null) {
|
|
2099
2103
|
throw new PerpError("RISK_VIOLATION", `${sym} is quarantined (${quarantined.strategy}: ${quarantined.reason})`, {
|
|
@@ -2273,7 +2277,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2273
2277
|
// 7. Execute both legs. Same exchange shares a nonce → sequential; else parallel.
|
|
2274
2278
|
if (!isJson())
|
|
2275
2279
|
console.log(chalk.gray(` Executing ${sameVenue ? "sequentially" : "both legs simultaneously"}...\n`));
|
|
2276
|
-
// The perp leg goes through placePerpVerified
|
|
2280
|
+
// The perp leg goes through placePerpVerified: a fill is the measured
|
|
2277
2281
|
// position delta; an unknown outcome is ORDER_PENDING (quarantine, never roll back).
|
|
2278
2282
|
let spotResult;
|
|
2279
2283
|
let perpResult;
|
|
@@ -2523,7 +2527,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2523
2527
|
remediation: `Pass both: perp arb close ${sym} --spot-exch <spot venue> --perp-exch <perp venue>`,
|
|
2524
2528
|
});
|
|
2525
2529
|
}
|
|
2526
|
-
// Load adapters (bot/arb spot scope
|
|
2530
|
+
// Load adapters (bot/arb spot scope)
|
|
2527
2531
|
const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
|
|
2528
2532
|
const spotAdapter = await requireSpotAdapter(spotDef.id, await getAdapter(spotDef.id));
|
|
2529
2533
|
const perpAdapter = await getAdapter(perpExch);
|
|
@@ -2551,12 +2555,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
2551
2555
|
// Read before anything moves — a failure aborts the close.
|
|
2552
2556
|
const preCloseQuote = await readSpotQuoteAvailableFresh(spotAdapter);
|
|
2553
2557
|
// Same exchange → sequential (nonce collision), different → parallel.
|
|
2554
|
-
// The spot leg's REAL fill decides the outcome
|
|
2558
|
+
// The spot leg's REAL fill decides the outcome: only a confirmed spot sale
|
|
2555
2559
|
// counts as closed; legs are re-matched so no naked short / unhedged spot is left.
|
|
2556
2560
|
const sameVenue = spotDef.id === (tryResolveVenueId(perpExch) ?? perpExch);
|
|
2557
2561
|
const closeDec = sizeDecimalsOf(closeSize);
|
|
2558
|
-
// The perp buy-back and every re-match order go through placePerpVerified
|
|
2559
|
-
//
|
|
2562
|
+
// The perp buy-back and every re-match order go through placePerpVerified:
|
|
2563
|
+
// only a measured position change counts; an unknown outcome stops the close.
|
|
2560
2564
|
let spotResult;
|
|
2561
2565
|
let perpResult;
|
|
2562
2566
|
if (sameVenue) {
|
|
@@ -3073,7 +3077,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
|
|
|
3073
3077
|
(p.long.dex === pos.shortDex && p.short.dex === pos.longDex)));
|
|
3074
3078
|
const currentSpread = current?.annualSpread ?? 0;
|
|
3075
3079
|
if (currentSpread <= closeSpread || !current) {
|
|
3076
|
-
// Quarantined
|
|
3080
|
+
// Quarantined: hands off until a human clears it.
|
|
3077
3081
|
if (isQuarantined(pos.underlying, (m) => console.error(chalk.red(m))))
|
|
3078
3082
|
continue;
|
|
3079
3083
|
const reason = !current ? "pair disappeared" : `spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`;
|
package/dist/commands/arb.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export declare function handleRates(isJson: () => boolean, opts: {
|
|
|
29
29
|
top?: string;
|
|
30
30
|
includeUnverified?: boolean;
|
|
31
31
|
}): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Cross-venue price basis — a PRICE comparison, independent of funding.
|
|
34
|
+
*
|
|
35
|
+
* Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
|
|
36
|
+
* its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
|
|
37
|
+
* come from each venue's market directory and are grouped by `canonical` symbol with the funding
|
|
38
|
+
* scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
|
|
39
|
+
* never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
|
|
40
|
+
* median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
|
|
41
|
+
* a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
|
|
42
|
+
* A venue that fails is reported on stderr and is absent from the scan, never silently empty.
|
|
43
|
+
*/
|
|
32
44
|
export declare function handleBasisScan(isJson: () => boolean, opts: {
|
|
33
45
|
minBasis: string;
|
|
34
46
|
symbol?: string;
|
package/dist/commands/arb.js
CHANGED
|
@@ -1,99 +1,13 @@
|
|
|
1
1
|
import { makeTable, formatUsd, formatPercent, formatPnl, printJson, jsonOk, withJsonErrors, logSettledRejections } from "../utils.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { toHourlyRate, annualizeHourlyRate, estimateHourlyFundingFromHourly } from "../funding.js";
|
|
4
|
-
import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
4
|
+
import { fetchAllFundingRates, fetchCanonicalPriceGroups, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
5
5
|
import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
|
|
6
6
|
import { getVenue, listVenuesWithRole, tryResolveVenueId } from "../exchanges/venue.js";
|
|
7
7
|
import "../exchanges/venues/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
9
|
+
import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchLighterOrderBookDetailsRaw, } from "../shared-api.js";
|
|
9
10
|
import { scanDexArb } from "../dex-asset-map.js";
|
|
10
|
-
async function fetchPacificaRates() {
|
|
11
|
-
const assets = await fetchPacificaPrices();
|
|
12
|
-
return assets.map(p => ({
|
|
13
|
-
exchange: "pacifica",
|
|
14
|
-
symbol: p.symbol,
|
|
15
|
-
fundingRate: p.funding,
|
|
16
|
-
markPrice: p.mark,
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
async function fetchHyperliquidRates() {
|
|
20
|
-
const assets = await fetchHyperliquidMeta();
|
|
21
|
-
return assets.map(a => ({
|
|
22
|
-
exchange: "hyperliquid",
|
|
23
|
-
symbol: a.symbol,
|
|
24
|
-
fundingRate: a.funding,
|
|
25
|
-
markPrice: a.markPx,
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
async function fetchAsterRates() {
|
|
29
|
-
// SSOT rule #2: do NOT silently coerce missing markPrice / lastFundingRate
|
|
30
|
-
// to 0 — that would corrupt comparisons against other DEXs. Skip symbols
|
|
31
|
-
// whose Aster row is malformed instead. An endpoint failure PROPAGATES: the
|
|
32
|
-
// caller (handleBasisScan) runs every DEX under Promise.allSettled and reports
|
|
33
|
-
// the rejection on stderr — Aster is never silently absent from the scan.
|
|
34
|
-
const res = await fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex");
|
|
35
|
-
if (!res.ok)
|
|
36
|
-
throw new Error(`Aster premiumIndex HTTP ${res.status}`);
|
|
37
|
-
const premiums = await res.json();
|
|
38
|
-
if (!Array.isArray(premiums))
|
|
39
|
-
throw new Error("Aster premiumIndex returned a non-array response");
|
|
40
|
-
const out = [];
|
|
41
|
-
for (const p of premiums) {
|
|
42
|
-
const rawSym = String(p.symbol ?? "");
|
|
43
|
-
if (!rawSym.endsWith("USDT"))
|
|
44
|
-
continue;
|
|
45
|
-
const fundingRate = Number(p.lastFundingRate);
|
|
46
|
-
const markPrice = Number(p.markPrice);
|
|
47
|
-
if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
out.push({
|
|
51
|
-
exchange: "aster",
|
|
52
|
-
symbol: rawSym.replace(/USDT$/, ""),
|
|
53
|
-
fundingRate,
|
|
54
|
-
markPrice,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
async function fetchLighterRates() {
|
|
60
|
-
// SSOT rule #2: error must propagate. Caller (handleBasisScan) uses
|
|
61
|
-
// Promise.allSettled to keep multi-DEX comparison alive when one DEX is
|
|
62
|
-
// down, with explicit stderr logging.
|
|
63
|
-
const [details, funding] = await Promise.all([
|
|
64
|
-
fetchLighterOrderBookDetails(),
|
|
65
|
-
fetchLighterFundingRates(),
|
|
66
|
-
]);
|
|
67
|
-
const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
|
|
68
|
-
const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
|
|
69
|
-
const rates = [];
|
|
70
|
-
for (const fr of funding) {
|
|
71
|
-
const symbol = fr.symbol || symMap.get(fr.marketId);
|
|
72
|
-
if (!symbol)
|
|
73
|
-
continue;
|
|
74
|
-
// Prefer fr.markPrice; fall back to orderBook last-trade as documented
|
|
75
|
-
// price-source preference (NOT error fallback). Skip if neither resolves
|
|
76
|
-
// — never publish a 0 markPrice downstream.
|
|
77
|
-
const directMark = fr.markPrice;
|
|
78
|
-
const fallbackMark = priceMap.get(fr.marketId);
|
|
79
|
-
let markPrice;
|
|
80
|
-
if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
|
|
81
|
-
markPrice = directMark;
|
|
82
|
-
}
|
|
83
|
-
else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
|
|
84
|
-
markPrice = fallbackMark;
|
|
85
|
-
}
|
|
86
|
-
if (markPrice === undefined || !Number.isFinite(fr.rate))
|
|
87
|
-
continue;
|
|
88
|
-
rates.push({
|
|
89
|
-
exchange: "lighter",
|
|
90
|
-
symbol,
|
|
91
|
-
fundingRate: fr.rate,
|
|
92
|
-
markPrice,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return rates;
|
|
96
|
-
}
|
|
97
11
|
// annualize moved to ../funding.ts — use annualizeRate() / computeAnnualSpread()
|
|
98
12
|
export function registerArbCommands(program, isJson, getAdapterForExchange) {
|
|
99
13
|
const arb = program.command("arb").description("Funding rate arbitrage & basis trading");
|
|
@@ -291,12 +205,15 @@ function printSnapshotTable(snapshot) {
|
|
|
291
205
|
return r ? formatPercent(r.fundingRate) : chalk.gray("-");
|
|
292
206
|
}),
|
|
293
207
|
spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`), trend,
|
|
294
|
-
s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
|
|
208
|
+
s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}${s.executable ? "" : chalk.gray(" (read-only)")}` : chalk.gray("-"),
|
|
295
209
|
formatAvgRate(bestRate?.historicalAvg?.avg8h), formatAvgRate(bestRate?.historicalAvg?.avg24h), formatAvgRate(bestRate?.historicalAvg?.avg7d),
|
|
296
210
|
];
|
|
297
211
|
});
|
|
298
|
-
console.log(makeTable(["Symbol", ...columns.map(v => v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
|
|
212
|
+
console.log(makeTable(["Symbol", ...columns.map(v => v.apiStatus.kind === "unofficial" ? `${v.displayName}*` : v.displayName), "Spread", "24h", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
|
|
299
213
|
console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.\n`));
|
|
214
|
+
const unofficial = columns.filter(v => v.apiStatus.kind === "unofficial");
|
|
215
|
+
if (unofficial.length > 0)
|
|
216
|
+
console.log(chalk.yellow(` * ${unofficial.map(v => v.displayName).join(", ")}: UNOFFICIAL API (undocumented, read-only; may change or break). Unverified funding — shown only with --include-unverified.\n`));
|
|
300
217
|
}
|
|
301
218
|
function printFundingExchangeStatus(snapshot) {
|
|
302
219
|
const statuses = Object.entries(snapshot.exchangeStatus).map(([ex, status]) => {
|
|
@@ -317,7 +234,7 @@ function printDetailedComparison(comparison) {
|
|
|
317
234
|
}
|
|
318
235
|
const spreadColor = comparison.maxSpreadAnnual >= 30 ? chalk.green.bold : comparison.maxSpreadAnnual >= 10 ? chalk.yellow : chalk.white;
|
|
319
236
|
console.log(`\n Max Spread: ${spreadColor(`${comparison.maxSpreadAnnual.toFixed(1)}%`)} annual`);
|
|
320
|
-
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
|
|
237
|
+
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}${comparison.executable ? "" : chalk.yellow(` — not executable: ${comparison.readOnlyLegs.join(", ")} is read-only in perp-cli`)}`);
|
|
321
238
|
console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional\n`);
|
|
322
239
|
}
|
|
323
240
|
// registerFundingSubcommands removed — use 'arb scan --rates/--compare/--history/--positions' instead
|
|
@@ -325,9 +242,13 @@ function printDetailedComparison(comparison) {
|
|
|
325
242
|
/** One-line stderr notice when rows of unverified-funding venues are shown (--include-unverified). */
|
|
326
243
|
function warnUnverifiedFunding(rates) {
|
|
327
244
|
const venues = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
|
|
328
|
-
if (venues.length
|
|
329
|
-
|
|
330
|
-
|
|
245
|
+
if (venues.length > 0) {
|
|
246
|
+
process.stderr.write(`[warn] ${venues.join(", ")}: funding rates are UNVERIFIED (unit not confirmed against funding payments; annualized values overstate venues whose funding accrues only during market hours). Shown on request — not for trading decisions.\n`);
|
|
247
|
+
}
|
|
248
|
+
const unofficial = [...new Set(rates.filter(r => r.apiStatus === "unofficial").map(r => r.exchange))];
|
|
249
|
+
if (unofficial.length > 0) {
|
|
250
|
+
process.stderr.write(`[warn] ${unofficial.join(", ")}: read through an UNOFFICIAL API (undocumented; may change or be blocked without notice). Rows carry apiStatus "unofficial".\n`);
|
|
251
|
+
}
|
|
331
252
|
}
|
|
332
253
|
export async function handleRates(isJson, opts) {
|
|
333
254
|
const minSpread = parseFloat(opts.minSpread);
|
|
@@ -373,59 +294,52 @@ export async function handleRates(isJson, opts) {
|
|
|
373
294
|
printSnapshotTable(snapshot);
|
|
374
295
|
printFundingExchangeStatus(snapshot);
|
|
375
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Cross-venue price basis — a PRICE comparison, independent of funding.
|
|
299
|
+
*
|
|
300
|
+
* Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
|
|
301
|
+
* its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
|
|
302
|
+
* come from each venue's market directory and are grouped by `canonical` symbol with the funding
|
|
303
|
+
* scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
|
|
304
|
+
* never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
|
|
305
|
+
* median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
|
|
306
|
+
* a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
|
|
307
|
+
* A venue that fails is reported on stderr and is absent from the scan, never silently empty.
|
|
308
|
+
*/
|
|
376
309
|
export async function handleBasisScan(isJson, opts) {
|
|
377
310
|
if (!isJson())
|
|
378
311
|
console.log(chalk.cyan(" Fetching prices for basis calculation...\n"));
|
|
379
|
-
// SSOT rule #2: each DEX is independent. allSettled keeps the comparison
|
|
380
|
-
// alive when one DEX errors out, while rejection reasons get logged to
|
|
381
|
-
// stderr (explicit failure, never silent [] downgrade).
|
|
382
|
-
const settled = await Promise.allSettled([
|
|
383
|
-
fetchPacificaRates(),
|
|
384
|
-
fetchHyperliquidRates(),
|
|
385
|
-
fetchLighterRates(),
|
|
386
|
-
fetchAsterRates(),
|
|
387
|
-
]);
|
|
388
|
-
logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter", "aster"], "basis-scan");
|
|
389
|
-
const pacRates = settled[0].status === "fulfilled" ? settled[0].value : [];
|
|
390
|
-
const hlRates = settled[1].status === "fulfilled" ? settled[1].value : [];
|
|
391
|
-
const ltRates = settled[2].status === "fulfilled" ? settled[2].value : [];
|
|
392
|
-
const astRates = settled[3].status === "fulfilled" ? settled[3].value : [];
|
|
393
|
-
const exchangePrices = new Map();
|
|
394
312
|
const filterSymbol = opts.symbol?.toUpperCase();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!exchangePrices.has(r.symbol))
|
|
401
|
-
exchangePrices.set(r.symbol, new Map());
|
|
402
|
-
exchangePrices.get(r.symbol).set(r.exchange, r.markPrice);
|
|
403
|
-
}
|
|
313
|
+
const snapshot = await fetchCanonicalPriceGroups(listVenuesWithRole("trading"), {
|
|
314
|
+
symbols: filterSymbol !== undefined ? [filterSymbol] : undefined, tag: "basis-scan",
|
|
315
|
+
});
|
|
316
|
+
for (const [venue, msg] of Object.entries(snapshot.exchangeErrors))
|
|
317
|
+
console.error(`[basis-scan] ${venue} fetch failed: ${msg}`);
|
|
404
318
|
const opps = [];
|
|
405
|
-
for (const [symbol,
|
|
406
|
-
if (
|
|
319
|
+
for (const [symbol, priced] of snapshot.groups) {
|
|
320
|
+
if (priced.length < 2)
|
|
407
321
|
continue;
|
|
408
|
-
let
|
|
409
|
-
for (const
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (price > maxPrice) {
|
|
415
|
-
maxPrice = price;
|
|
416
|
-
maxEx = ex;
|
|
417
|
-
}
|
|
322
|
+
let min = priced[0], max = priced[0];
|
|
323
|
+
for (const r of priced) {
|
|
324
|
+
if (r.markPrice < min.markPrice)
|
|
325
|
+
min = r;
|
|
326
|
+
if (r.markPrice > max.markPrice)
|
|
327
|
+
max = r;
|
|
418
328
|
}
|
|
419
|
-
if (
|
|
329
|
+
if (min.exchange === max.exchange)
|
|
420
330
|
continue;
|
|
421
|
-
const diff =
|
|
422
|
-
const avgPrice = (
|
|
331
|
+
const diff = max.markPrice - min.markPrice;
|
|
332
|
+
const avgPrice = (max.markPrice + min.markPrice) / 2;
|
|
423
333
|
const pctDiff = (diff / avgPrice) * 100;
|
|
334
|
+
if (!(pctDiff <= MAX_PRICE_DEVIATION_PCT)) {
|
|
335
|
+
console.error(`[basis-scan] excluded ${symbol} ${min.exchange}↔${max.exchange}: mark gap ${pctDiff.toFixed(1)}% > ${MAX_PRICE_DEVIATION_PCT}% (likely a different asset with the same ticker)`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
424
338
|
if (pctDiff * 365 >= parseFloat(opts.minBasis)) {
|
|
425
339
|
const priceObj = {};
|
|
426
|
-
for (const
|
|
427
|
-
priceObj[
|
|
428
|
-
opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn:
|
|
340
|
+
for (const r of priced)
|
|
341
|
+
priceObj[r.exchange] = r.markPrice;
|
|
342
|
+
opps.push({ symbol, prices: priceObj, priceDiff: diff, pctDiff, buyOn: min.exchange, sellOn: max.exchange });
|
|
429
343
|
}
|
|
430
344
|
}
|
|
431
345
|
opps.sort((a, b) => b.pctDiff - a.pctDiff);
|
|
@@ -439,12 +353,12 @@ export async function handleBasisScan(isJson, opts) {
|
|
|
439
353
|
console.log(chalk.gray(" Strategy: Buy cheap, sell expensive, profit from convergence.\n"));
|
|
440
354
|
const rows = opps.map((o) => [
|
|
441
355
|
chalk.white.bold(o.symbol),
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
o.prices.lighter ? `$${formatUsd(o.prices.lighter)}` : chalk.gray("-"),
|
|
356
|
+
`$${formatUsd(o.prices[o.buyOn])}`,
|
|
357
|
+
`$${formatUsd(o.prices[o.sellOn])}`,
|
|
445
358
|
`$${formatUsd(o.priceDiff)}`, `${o.pctDiff.toFixed(4)}%`, chalk.green(o.buyOn), chalk.red(o.sellOn),
|
|
359
|
+
chalk.gray(String(Object.keys(o.prices).length)),
|
|
446
360
|
]);
|
|
447
|
-
console.log(makeTable(["Symbol", "
|
|
361
|
+
console.log(makeTable(["Symbol", "Buy Price", "Sell Price", "Diff", "Basis %", "Buy On", "Sell On", "Venues"], rows));
|
|
448
362
|
}
|
|
449
363
|
export async function handleDexScan(isJson, opts) {
|
|
450
364
|
if (!isJson())
|
|
@@ -497,9 +411,13 @@ export async function handleCompare(isJson, symbol, includeUnverified = false) {
|
|
|
497
411
|
if (comparison)
|
|
498
412
|
warnUnverifiedFunding(comparison.rates);
|
|
499
413
|
if (!comparison) {
|
|
414
|
+
// A venue ticker whose canonical symbol differs (Variational US500-PERP = SPY): say where to look.
|
|
415
|
+
const hint = (await import("../exchanges/variational/public.js")).canonicalHintFor(symbol);
|
|
500
416
|
if (isJson())
|
|
501
|
-
return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
|
|
417
|
+
return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false, ...(hint !== null ? { hint } : {}) }));
|
|
502
418
|
console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
|
|
419
|
+
if (hint !== null)
|
|
420
|
+
console.log(chalk.yellow(` Hint: ${hint}\n`));
|
|
503
421
|
return;
|
|
504
422
|
}
|
|
505
423
|
if (isJson())
|
|
@@ -594,7 +512,7 @@ export async function handleFundingPositions(isJson, getAdapterForExchange, opts
|
|
|
594
512
|
}
|
|
595
513
|
const sz = Math.abs(Number(pos.size));
|
|
596
514
|
const nu = sz * mk;
|
|
597
|
-
const fh = fd ? fd.fundingHours : null; /* market basis first
|
|
515
|
+
const fh = fd ? fd.fundingHours : null; /* market basis first */
|
|
598
516
|
const hr = fh !== null ? fr / fh : toHourlyRate(fr, exchange);
|
|
599
517
|
const ap = annualizeHourlyRate(hr);
|
|
600
518
|
const hp = estimateHourlyFundingFromHourly(hr, nu, pos.side);
|
package/dist/commands/bot.js
CHANGED
|
@@ -419,7 +419,7 @@ export function registerStrategyCommands(program, getAdapter, getAdapterFor, isJ
|
|
|
419
419
|
}
|
|
420
420
|
console.log();
|
|
421
421
|
});
|
|
422
|
-
// ── quarantine
|
|
422
|
+
// ── quarantine: positions the bot refuses to touch until a human clears them ──
|
|
423
423
|
bot
|
|
424
424
|
.command("quarantine")
|
|
425
425
|
.description("List quarantined symbols (unknown / unverified order outcome or unhedged leg) — blocked for every bot strategy, arb exec and arb auto (~/.perp/bot-quarantine.json)")
|
package/dist/commands/builder.js
CHANGED
|
@@ -283,7 +283,7 @@ export function registerWalletBuilderCommands(wallet, isJson, getAdapterForExcha
|
|
|
283
283
|
.action(async (venueArg, _opts, cmd) => {
|
|
284
284
|
await withJsonErrors(isJson(), async () => {
|
|
285
285
|
const network = networkOf(cmd);
|
|
286
|
-
const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus"]];
|
|
286
|
+
const venues = venueArg !== undefined ? [getVenue(venueArg).id] : [...builderFeeVenues(), ...["lighter", "lighter-rh", "arcus", "risex", "txflow", "variational", "grvt", "extended"]];
|
|
287
287
|
const rows = [];
|
|
288
288
|
for (const v of venues) {
|
|
289
289
|
const configured = builderTargetOf(loadSettings().builder, v) !== null;
|