perp-cli 0.12.17 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +142 -55
- package/skills/perp-cli/references/agent-operations.md +110 -56
- package/skills/perp-cli/references/commands.md +266 -71
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -8,27 +8,36 @@ import { logExecution } from "../../execution-log.js";
|
|
|
8
8
|
import { invalidateCache } from "../../cache.js";
|
|
9
9
|
import { checkSpotPerpLiquidity } from "../../liquidity.js";
|
|
10
10
|
import { computeSpotPerpMatchedSize } from "../../arb-sizing.js";
|
|
11
|
-
import {
|
|
11
|
+
import { findSpotQuoteBalance, getBotSpotAdapter, spotNetworkOf, spotTokenBase } from "../../exchanges/spot-factory.js";
|
|
12
|
+
import { getVenue } from "../../exchanges/venue.js";
|
|
13
|
+
import { PerpError } from "../../errors.js";
|
|
14
|
+
import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../../exchanges/spot-result.js";
|
|
15
|
+
import { floorToStep, lotStepOf } from "../../exchanges/precision.js";
|
|
16
|
+
import { closePerpVerified, isOutcomeUnknown, placePerpVerified } from "../perp-order.js";
|
|
17
|
+
import { transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, matchSymbol, getPriceEstimate, isPositionGone, } from "./funding-arb-utils.js";
|
|
12
18
|
// ── Executor ──
|
|
13
19
|
export class SpotPerpExecutor {
|
|
14
|
-
|
|
15
|
-
/** Get or create cached spot adapter. */
|
|
20
|
+
/** Spot adapter for a bot spot venue (factory-cached); null outside the bot spot scope. */
|
|
16
21
|
async getSpotAdapter(name, adapter) {
|
|
17
|
-
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
const spot = await getSpotAdapterUtil(name, adapter);
|
|
21
|
-
if (spot)
|
|
22
|
-
this._spotAdapterCache.set(name, spot);
|
|
23
|
-
return spot;
|
|
22
|
+
return getBotSpotAdapter(name, adapter);
|
|
24
23
|
}
|
|
25
|
-
/** Transfer
|
|
26
|
-
async transferToSpot(spotAdapter,
|
|
27
|
-
|
|
24
|
+
/** Transfer the spot quote asset perp -> spot. false = shared margin (nothing moved). */
|
|
25
|
+
async transferToSpot(spotAdapter, amount) {
|
|
26
|
+
return transferUsdcToSpot(spotAdapter, amount);
|
|
28
27
|
}
|
|
29
|
-
/** Transfer
|
|
30
|
-
async transferToPerp(spotAdapter,
|
|
31
|
-
|
|
28
|
+
/** Transfer the spot quote asset spot -> perp. false = shared margin (nothing moved). */
|
|
29
|
+
async transferToPerp(spotAdapter, amount) {
|
|
30
|
+
return transferUsdcToPerp(spotAdapter, amount);
|
|
31
|
+
}
|
|
32
|
+
/** Rollback transfer spot → perp: failure is logged loudly (funds remain in spot), never swallowed. */
|
|
33
|
+
async returnToPerp(spotAdapter, amount, spotExchange, log) {
|
|
34
|
+
try {
|
|
35
|
+
if (await transferUsdcToPerp(spotAdapter, amount))
|
|
36
|
+
log(` [SPA] Recovered $${amount} back to perp`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
log(` [SPA] WARNING: $${amount} left in ${spotExchange} spot — transfer back to perp failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
}
|
|
32
41
|
}
|
|
33
42
|
// isPositionGone() is now in funding-arb-utils.ts
|
|
34
43
|
/**
|
|
@@ -66,31 +75,51 @@ export class SpotPerpExecutor {
|
|
|
66
75
|
const spotMarkets = await spotAdapter.getSpotMarkets();
|
|
67
76
|
const spotDec = spotMarkets.find(m => m.baseToken.toUpperCase() === symbol.toUpperCase())?.sizeDecimals;
|
|
68
77
|
const perpMarkets = await perpAdapter.getMarkets();
|
|
69
|
-
const
|
|
78
|
+
const perpMarket = perpMarkets.find(m => matchSymbol(m.symbol, perpSymbol) || matchSymbol(m.symbol, symbol));
|
|
79
|
+
let perpDec;
|
|
80
|
+
if (perpMarket !== undefined) {
|
|
81
|
+
// Venue listed the market but did not publish its size precision — sizing would be a guess.
|
|
82
|
+
if (perpMarket.sizeDecimals === null) {
|
|
83
|
+
log(` [SPA] Skip ${symbol}: ${perpExchange} does not publish sizeDecimals for ${perpMarket.symbol}`);
|
|
84
|
+
return { success: false, error: "perp sizeDecimals unknown" };
|
|
85
|
+
}
|
|
86
|
+
perpDec = perpMarket.sizeDecimals;
|
|
87
|
+
}
|
|
70
88
|
const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, price, spotExchange, perpExchange, spotDec, perpDec);
|
|
71
89
|
if (!matched) {
|
|
72
90
|
log(` [SPA] Skip ${symbol}: matched size fail (spotDec=${spotDec}, perpDec=${perpDec}, price=${price.toFixed(2)}, sizeUsd=${liq.adjustedSizeUsd.toFixed(2)})`);
|
|
73
91
|
return { success: false, error: "matched size fail" };
|
|
74
92
|
}
|
|
75
93
|
log(` [SPA] Opening: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}) ${spotExchange}-spot<>${perpExchange}`);
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
let
|
|
94
|
+
// Lot steps from the venue market info: the hedge is floored to the perp lot and every fill is
|
|
95
|
+
// judged with a half-lot tolerance. Unknown → stop before any transfer / order (Rule #2).
|
|
96
|
+
let perpStep;
|
|
97
|
+
let spotStep;
|
|
79
98
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
perpStep = await lotStepOf(perpAdapter, perpSymbol);
|
|
100
|
+
spotStep = await spotLotStepOf(spotAdapter, symbol);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
+
log(` [SPA] Skip ${symbol}: lot step unknown (${msg})`);
|
|
105
|
+
return { success: false, error: `lot step: ${msg}` };
|
|
83
106
|
}
|
|
84
|
-
|
|
107
|
+
// Transfer USDC to spot account — only transfer the shortfall, track actual amount
|
|
108
|
+
const tAmt = Math.ceil(matched.notional * 1.02);
|
|
109
|
+
const spotDef = getVenue(spotExchange);
|
|
110
|
+
const quoteBal = findSpotQuoteBalance(spotDef, spotNetworkOf(spotAdapter), await spotAdapter.getSpotBalances());
|
|
111
|
+
const spotUsdcAvailable = quoteBal ? parseFloat(quoteBal.available) : 0;
|
|
85
112
|
let actualTransferred = 0;
|
|
86
113
|
if (spotUsdcAvailable >= tAmt) {
|
|
87
|
-
log(` [SPA] Spot already has $${spotUsdcAvailable.toFixed(2)}
|
|
114
|
+
log(` [SPA] Spot already has $${spotUsdcAvailable.toFixed(2)} ${quoteBal?.token ?? ""}, skipping transfer`);
|
|
88
115
|
}
|
|
89
116
|
else {
|
|
90
|
-
|
|
117
|
+
const want = Math.ceil(tAmt - spotUsdcAvailable);
|
|
91
118
|
try {
|
|
92
|
-
await transferUsdcToSpot(spotAdapter,
|
|
93
|
-
|
|
119
|
+
if (await transferUsdcToSpot(spotAdapter, want)) {
|
|
120
|
+
actualTransferred = want;
|
|
121
|
+
log(` [SPA] Transferred $${actualTransferred} to ${spotExchange} spot`);
|
|
122
|
+
}
|
|
94
123
|
}
|
|
95
124
|
catch (err) {
|
|
96
125
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -98,77 +127,113 @@ export class SpotPerpExecutor {
|
|
|
98
127
|
return { success: false, error: `transfer: ${msg}` };
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
|
-
//
|
|
130
|
+
// Pre-trade base balance: the post-buy check measures the DELTA, not the total
|
|
131
|
+
// (pre-existing holdings must not count as "bought"). A failed read aborts before buying.
|
|
132
|
+
let preBase;
|
|
133
|
+
try {
|
|
134
|
+
preBase = await this.baseBalance(spotAdapter, symbol);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
138
|
+
log(` [SPA] Pre-trade ${symbol} balance read failed: ${msg} — aborting before the spot buy`);
|
|
139
|
+
if (actualTransferred > 0)
|
|
140
|
+
await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
|
|
141
|
+
return { success: false, error: `pre-trade balance: ${msg}` };
|
|
142
|
+
}
|
|
143
|
+
// Step 1: Buy spot — the hedge is sized to what actually filled (§12.4 #5)
|
|
144
|
+
let fill;
|
|
102
145
|
try {
|
|
103
|
-
await spotAdapter
|
|
104
|
-
|
|
146
|
+
fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
|
|
147
|
+
if (fill.status === "partial")
|
|
148
|
+
log(` [SPA] Spot buy partial: ${fill.filledSize} of ${matched.size} ${symbol} — hedging the filled size`);
|
|
149
|
+
logExecution({ type: "arb_entry", exchange: `${spotExchange}-spot`, symbol, side: "buy", size: String(fill.filledSize), notional: matched.notional, status: "success", dryRun: false, meta: { mode: "spot-perp", leg: "spot", fillStatus: fill.status, requestedSize: matched.size } });
|
|
105
150
|
}
|
|
106
151
|
catch (err) {
|
|
107
152
|
const msg = err instanceof Error ? err.message : String(err);
|
|
108
153
|
log(` [SPA] Spot buy failed: ${msg}`);
|
|
154
|
+
if (err instanceof PerpError && err.structured.code === "ORDER_PENDING") {
|
|
155
|
+
// Outcome unknown: the quote may be spent — do not move funds back or retry.
|
|
156
|
+
log(` [SPA] CRITICAL: spot buy ${symbol} on ${spotExchange} is PENDING — no perp hedge placed. Verify the spot balance manually; do NOT resubmit.`);
|
|
157
|
+
return { success: false, error: `spot buy ${matched.size} on ${spotExchange} pending (no perp hedge placed): ${msg}`, manual: true };
|
|
158
|
+
}
|
|
109
159
|
if (actualTransferred > 0) {
|
|
110
|
-
|
|
111
|
-
await transferUsdcToPerp(spotAdapter, spotExchange, actualTransferred);
|
|
112
|
-
log(` [SPA] Recovered $${actualTransferred} USDC back to perp`);
|
|
113
|
-
}
|
|
114
|
-
catch { /* best effort */ }
|
|
160
|
+
await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
|
|
115
161
|
}
|
|
116
162
|
return { success: false, error: `spot buy: ${msg}` };
|
|
117
163
|
}
|
|
118
|
-
// Step 1b: Verify
|
|
164
|
+
// Step 1b: Verify the buy by balance delta vs the pre-trade balance. A failed
|
|
165
|
+
// verification STOPS (no perp short on an unverified spot leg).
|
|
166
|
+
const hedgeSize = floorToStep(fill.filledSize, perpStep);
|
|
119
167
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (boughtAmt < parseFloat(matched.size) * 0.8) {
|
|
125
|
-
log(` [SPA] Spot buy returned OK but ${symbol} balance too low (${boughtAmt.toFixed(4)} < ${matched.size} × 80%) — aborting`);
|
|
126
|
-
if (actualTransferred > 0) {
|
|
127
|
-
try {
|
|
128
|
-
await transferUsdcToPerp(spotAdapter, spotExchange, actualTransferred);
|
|
129
|
-
}
|
|
130
|
-
catch { /* best effort */ }
|
|
131
|
-
}
|
|
132
|
-
return { success: false, error: "spot buy not confirmed by balance" };
|
|
168
|
+
const delta = (await this.baseBalance(spotAdapter, symbol)) - preBase;
|
|
169
|
+
if (delta < fill.filledSize * 0.8) {
|
|
170
|
+
log(` [SPA] CRITICAL: spot buy reported ${fill.filledSize} ${symbol} but the balance rose only ${delta.toFixed(6)} — no perp hedge placed; verify ${spotExchange} spot manually`);
|
|
171
|
+
return { success: false, error: `spot buy reported ${fill.filledSize} but the balance rose only ${delta.toFixed(6)} (no perp hedge placed)`, manual: true };
|
|
133
172
|
}
|
|
134
173
|
}
|
|
135
|
-
catch {
|
|
136
|
-
|
|
174
|
+
catch (err) {
|
|
175
|
+
log(` [SPA] CRITICAL: post-buy balance check failed (${err instanceof Error ? err.message : String(err)}) — stopping: ${fill.filledSize} ${symbol} spot is unhedged; verify manually`);
|
|
176
|
+
return { success: false, error: `post-buy balance check failed — ${fill.filledSize} spot unhedged`, manual: true };
|
|
177
|
+
}
|
|
178
|
+
if (!(Number(hedgeSize) > 0)) {
|
|
179
|
+
log(` [SPA] CRITICAL: spot buy ${symbol} filled only dust (${fill.filledSize}, below one ${perpExchange} lot ${perpStep}) — unhedged spot remains on ${spotExchange}`);
|
|
180
|
+
return { success: false, error: `spot fill ${fill.filledSize} below one perp lot (${perpStep}) — unhedged spot remains`, manual: true };
|
|
181
|
+
}
|
|
182
|
+
// Step 2: Short perp (filled size only), verified by the position delta.
|
|
137
183
|
try {
|
|
138
|
-
await perpAdapter
|
|
139
|
-
logExecution({ type: "arb_entry", exchange: perpExchange, symbol, side: "sell", size:
|
|
184
|
+
const pf = await placePerpVerified(perpAdapter, perpExchange, perpSymbol, "sell", hedgeSize, { stepSize: perpStep });
|
|
185
|
+
logExecution({ type: "arb_entry", exchange: perpExchange, symbol, side: "sell", size: hedgeSize, notional: matched.notional, status: "success", dryRun: false, meta: { mode: "spot-perp", leg: "perp", measuredFill: pf.filledSize } });
|
|
186
|
+
// An accepted partial short (≥ 80% measured, or a venue-final partial IOC fill) leaves
|
|
187
|
+
// part of the spot unhedged: never resubmit — stop for a human (caller quarantines).
|
|
188
|
+
const shorted = floorToStep(pf.filledSize, perpStep);
|
|
189
|
+
if (Number(shorted) < Number(hedgeSize)) {
|
|
190
|
+
log(` [SPA] CRITICAL: perp short filled ${shorted} of ${hedgeSize} ${symbol} on ${perpExchange} — ${floorToStep(Number(hedgeSize) - Number(shorted), perpStep)} spot unhedged`);
|
|
191
|
+
return { success: false, error: `perp short filled ${shorted} of ${hedgeSize} on ${perpExchange}; ${hedgeSize} ${symbol} spot held on ${spotExchange} — spot partly unhedged`, manual: true };
|
|
192
|
+
}
|
|
140
193
|
}
|
|
141
194
|
catch (err) {
|
|
142
195
|
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
+
if (isOutcomeUnknown(err)) {
|
|
197
|
+
// The short may exist: selling the spot now could leave a naked short. Stop.
|
|
198
|
+
log(` [SPA] CRITICAL: perp short ${hedgeSize} ${symbol} on ${perpExchange} outcome unknown — spot NOT rolled back`);
|
|
199
|
+
return { success: false, error: `perp short outcome unknown (${msg}); ${hedgeSize} ${symbol} spot held on ${spotExchange} — verify the ${perpExchange} position, do NOT resubmit`, manual: true };
|
|
200
|
+
}
|
|
143
201
|
log(` [SPA] Perp short failed: ${msg} -- ROLLING BACK spot`);
|
|
144
202
|
try {
|
|
145
|
-
await spotAdapter
|
|
146
|
-
logExecution({ type: "multi_leg_rollback", exchange: `${spotExchange}-spot`, symbol, side: "sell", size:
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch { /* best effort */ }
|
|
203
|
+
const rb = await spotMarketFill(spotAdapter, symbol, "sell", hedgeSize, spotStep);
|
|
204
|
+
logExecution({ type: "multi_leg_rollback", exchange: `${spotExchange}-spot`, symbol, side: "sell", size: String(rb.filledSize), status: rb.status === "partial" ? "failed" : "success", dryRun: false });
|
|
205
|
+
if (rb.status === "partial") {
|
|
206
|
+
log(` [SPA] CRITICAL: spot rollback sold only ${rb.filledSize} of ${hedgeSize} ${symbol} — unhedged spot remains`);
|
|
207
|
+
return { success: false, error: `perp short failed (${msg}); rollback sold only ${rb.filledSize} of ${hedgeSize} — unhedged spot remains`, manual: true };
|
|
152
208
|
}
|
|
209
|
+
if (actualTransferred > 0)
|
|
210
|
+
await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
|
|
153
211
|
}
|
|
154
212
|
catch (rbErr) {
|
|
155
|
-
|
|
213
|
+
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
214
|
+
log(` [SPA] CRITICAL: Spot rollback failed: ${rbMsg}`);
|
|
215
|
+
return { success: false, error: `perp short failed (${msg}); spot rollback failed (${rbMsg}) — unhedged spot ${hedgeSize} remains`, manual: true };
|
|
156
216
|
}
|
|
157
217
|
return { success: false, error: `perp short: ${msg}` };
|
|
158
218
|
}
|
|
159
|
-
// Position verification
|
|
219
|
+
// Position verification (+ the venue's entry price of the short, recorded in the arb state)
|
|
160
220
|
invalidateCache("acct");
|
|
221
|
+
let perpEntryPrice = null;
|
|
161
222
|
try {
|
|
162
223
|
const perpPositions = await perpAdapter.getPositions();
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
224
|
+
const perpPos = perpPositions.find(x => matchSymbol(x.symbol, perpSymbol) && parseFloat(x.size) !== 0);
|
|
225
|
+
if (!perpPos) {
|
|
165
226
|
log(` [SPA] WARNING: Perp position not visible (cache delay?)`);
|
|
166
227
|
}
|
|
228
|
+
else {
|
|
229
|
+
const px = Number(perpPos.entryPrice);
|
|
230
|
+
perpEntryPrice = Number.isFinite(px) && px > 0 ? px : null;
|
|
231
|
+
}
|
|
167
232
|
}
|
|
168
233
|
catch (err) {
|
|
169
234
|
log(` [SPA] Verify error: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
235
|
}
|
|
171
|
-
return { success: true, size:
|
|
236
|
+
return { success: true, size: hedgeSize, notional: matched.notional, spotSize: fill.filledSize, spotAvgPrice: fill.avgPrice, perpEntryPrice };
|
|
172
237
|
}
|
|
173
238
|
catch (err) {
|
|
174
239
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -181,126 +246,260 @@ export class SpotPerpExecutor {
|
|
|
181
246
|
* 1. Close perp short FIRST (reduceOnly) — hedge stays intact if this fails
|
|
182
247
|
* 2. Sell spot
|
|
183
248
|
* 3. Transfer USDC back to perp (always, not just same-exchange)
|
|
249
|
+
*
|
|
250
|
+
* Returns `manual: true` (caller quarantines, never retries) when the spot outcome is
|
|
251
|
+
* unknown (pending sell, failed post-trade verification) or spot is left unhedged.
|
|
252
|
+
* A perp short is re-opened only when THIS call closed it; if the perp was already
|
|
253
|
+
* gone and spot cannot be sold, nothing new is opened — the position is quarantined.
|
|
184
254
|
*/
|
|
185
255
|
async closePosition(params) {
|
|
186
256
|
const { symbol, spotExchange, perpExchange, spotAdapter, perpAdapter, size, log } = params;
|
|
187
257
|
const perpSym = getPerpSymbol(symbol, perpExchange);
|
|
258
|
+
const notClosed = { closed: false, remainingSize: null, manual: false };
|
|
259
|
+
const manual = (reason) => {
|
|
260
|
+
log(` [SPA] CRITICAL: ${symbol} ${spotExchange}-spot<>${perpExchange} needs manual action — ${reason}`);
|
|
261
|
+
return { closed: false, remainingSize: null, manual: true, reason };
|
|
262
|
+
};
|
|
188
263
|
try {
|
|
189
|
-
//
|
|
264
|
+
// Pre-trade base + quote balances (post-sell delta check, proceeds sweep); read before anything moves.
|
|
265
|
+
let preBase;
|
|
266
|
+
let preQuote;
|
|
267
|
+
try {
|
|
268
|
+
const pre = await readSpotBalancesFresh(spotAdapter);
|
|
269
|
+
preBase = this.baseOf(spotAdapter, pre, symbol);
|
|
270
|
+
preQuote = this.quoteOf(spotExchange, spotAdapter, pre);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
log(` [SPA] Pre-close ${symbol} balance read failed: ${err instanceof Error ? err.message : String(err)} — close not started`);
|
|
274
|
+
return notClosed;
|
|
275
|
+
}
|
|
276
|
+
// Step 1: Close perp short FIRST (reduceOnly, verified, one residual close) — if this fails, hedge is still intact
|
|
277
|
+
const dec = sizeDecimalsOf(size);
|
|
278
|
+
let perpClosedHere = false;
|
|
279
|
+
/** Base units of the short this call closed (measured). */
|
|
280
|
+
let perpClosed = 0;
|
|
281
|
+
/** Set when the perp close left a residual short open (after one residual close). */
|
|
282
|
+
let perpResidual = null;
|
|
190
283
|
try {
|
|
191
|
-
await perpAdapter
|
|
192
|
-
|
|
284
|
+
const pc = await closePerpVerified(perpAdapter, perpExchange, perpSym, "buy", size);
|
|
285
|
+
perpClosedHere = true;
|
|
286
|
+
perpClosed = Math.min(pc.closed, Number(size));
|
|
287
|
+
if (pc.residual > 0)
|
|
288
|
+
perpResidual = pc;
|
|
289
|
+
logExecution({ type: "arb_close", exchange: perpExchange, symbol, side: "buy", size, status: pc.residual > 0 ? "failed" : "success", dryRun: false, meta: { mode: "spot-perp", measuredClose: pc.closed, residual: pc.residual } });
|
|
193
290
|
}
|
|
194
291
|
catch (err) {
|
|
195
292
|
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
if (isOutcomeUnknown(err)) {
|
|
294
|
+
return manual(`perp close ${size} on ${perpExchange} outcome unknown (${msg}); spot not sold — verify the perp position, do NOT resubmit`);
|
|
295
|
+
}
|
|
196
296
|
// ReduceOnly rejected = position already gone, proceed to sell spot
|
|
197
|
-
if (isPositionGone(
|
|
297
|
+
if (isPositionGone(err)) {
|
|
198
298
|
log(` [SPA] Perp already closed (ReduceOnly rejected)`);
|
|
199
299
|
}
|
|
200
300
|
else {
|
|
201
301
|
log(` [SPA] Perp close failed, hedge intact: ${msg}`);
|
|
202
|
-
return
|
|
302
|
+
return notClosed;
|
|
203
303
|
}
|
|
204
304
|
}
|
|
205
|
-
// Step 2: Sell spot
|
|
305
|
+
// Step 2: Sell spot — sized to the VERIFIED perp close when a perp residual remains
|
|
306
|
+
// (the unsold spot then still hedges the residual short; quarantined below).
|
|
307
|
+
const sellSize = perpResidual === null ? size : fillSizeString(perpClosed, dec);
|
|
308
|
+
const closedStr = fillSizeString(perpClosed, dec);
|
|
309
|
+
const residualNote = perpResidual === null ? "" : `perp close on ${perpExchange} closed ${closedStr} of ${size}, ${perpResidual.residual} short still open${perpResidual.note !== null ? ` (${perpResidual.note})` : ""}`;
|
|
310
|
+
if (perpResidual !== null) {
|
|
311
|
+
log(` [SPA] ${residualNote} — selling only ${sellSize} spot`);
|
|
312
|
+
if (!(Number(sellSize) > 0))
|
|
313
|
+
return manual(`${residualNote}; spot not sold`);
|
|
314
|
+
}
|
|
315
|
+
let fill;
|
|
206
316
|
try {
|
|
207
|
-
await spotAdapter
|
|
317
|
+
fill = await spotMarketFill(spotAdapter, symbol, "sell", sellSize);
|
|
208
318
|
}
|
|
209
319
|
catch (err) {
|
|
210
320
|
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
-
|
|
321
|
+
if (err instanceof PerpError && err.structured.code === "ORDER_PENDING") {
|
|
322
|
+
// The sell may still execute: re-hedging now could leave a naked short.
|
|
323
|
+
return manual(`spot sell ${sellSize} is PENDING (${msg}); perp is closed — verify ${spotExchange} spot, do NOT resubmit`);
|
|
324
|
+
}
|
|
325
|
+
if (!perpClosedHere) {
|
|
326
|
+
// The perp was already gone before this call: opening a new short here is not a
|
|
327
|
+
// rollback, it is a new position. Leave it to a human.
|
|
328
|
+
return manual(`spot sell ${size} failed (${msg}) and the perp short was already gone — ${size} spot is unhedged`);
|
|
329
|
+
}
|
|
330
|
+
// Definite spot failure (nothing sold): restore the short this call closed (measured), verified.
|
|
331
|
+
log(` [SPA] Spot sell failed: ${msg} -- re-opening perp hedge ${closedStr}`);
|
|
212
332
|
try {
|
|
213
|
-
await perpAdapter
|
|
333
|
+
const rh = await placePerpVerified(perpAdapter, perpExchange, perpSym, "sell", closedStr);
|
|
334
|
+
if (rh.filledSize < Number(closedStr) * (1 - 1e-6)) {
|
|
335
|
+
return manual(`spot sell failed (${msg}); the perp re-hedge filled ${rh.filledSize} of ${closedStr} — spot partly unhedged`);
|
|
336
|
+
}
|
|
214
337
|
}
|
|
215
338
|
catch (rbErr) {
|
|
216
|
-
|
|
339
|
+
return manual(`spot sell failed (${msg}) and the perp re-hedge failed or is unconfirmed (${rbErr instanceof Error ? rbErr.message : String(rbErr)}) — ${closedStr} spot may be unhedged`);
|
|
217
340
|
}
|
|
218
|
-
|
|
341
|
+
if (perpResidual !== null)
|
|
342
|
+
return manual(`${residualNote}; spot sell failed (${msg}) and the closed part was re-hedged`);
|
|
343
|
+
return notClosed;
|
|
219
344
|
}
|
|
220
|
-
// Step 2b: Verify
|
|
345
|
+
// Step 2b: Verify the sell by a fresh balance delta vs the pre-trade balance; a failed check stops.
|
|
221
346
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
347
|
+
const sold = preBase - this.baseOf(spotAdapter, await readSpotBalancesFresh(spotAdapter), symbol);
|
|
348
|
+
if (sold < fill.filledSize * 0.8) {
|
|
349
|
+
return manual(`spot sell reported ${fill.filledSize} but the balance fell only ${sold.toFixed(6)}; perp is closed — verify ${spotExchange} spot`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
return manual(`post-sell balance check failed (${err instanceof Error ? err.message : String(err)}); perp is closed — verify ${spotExchange} spot`);
|
|
354
|
+
}
|
|
355
|
+
// Partial sell: re-hedge the unsold remainder so it stays a hedged position.
|
|
356
|
+
let remainingSize = null;
|
|
357
|
+
if (fill.status === "partial") {
|
|
358
|
+
const remaining = fillSizeString(Number(sellSize) - fill.filledSize, dec);
|
|
359
|
+
if (Number(remaining) > 0) {
|
|
360
|
+
log(` [SPA] Spot sell partial: ${fill.filledSize} of ${sellSize} ${symbol} — re-hedging the remaining ${remaining}`);
|
|
228
361
|
try {
|
|
229
|
-
await perpAdapter
|
|
362
|
+
const rh = await placePerpVerified(perpAdapter, perpExchange, perpSym, "sell", remaining);
|
|
363
|
+
if (rh.filledSize < Number(remaining) * (1 - 1e-6)) {
|
|
364
|
+
return manual(`spot sell partial (${fill.filledSize} of ${sellSize}); re-hedge of the unsold ${remaining} filled ${rh.filledSize} — spot partly unhedged`);
|
|
365
|
+
}
|
|
366
|
+
remainingSize = remaining;
|
|
230
367
|
}
|
|
231
368
|
catch (rbErr) {
|
|
232
|
-
|
|
369
|
+
return manual(`re-hedge of the unsold ${remaining} failed or is unconfirmed (${rbErr instanceof Error ? rbErr.message : String(rbErr)}) — unhedged spot may remain`);
|
|
233
370
|
}
|
|
234
|
-
return false;
|
|
235
371
|
}
|
|
236
372
|
}
|
|
237
|
-
|
|
238
|
-
//
|
|
373
|
+
// Step 3: Transfer THIS sale's proceeds back to perp (quote delta since the pre-close
|
|
374
|
+
// read — never quote that was already in spot, e.g. another position's funds).
|
|
239
375
|
try {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
await transferUsdcToPerp(spotAdapter, spotExchange, Math.floor(usdcAmount));
|
|
245
|
-
log(` [SPA] Transferred $${Math.floor(usdcAmount)} USDC back to perp`);
|
|
376
|
+
const postQuote = this.quoteOf(spotExchange, spotAdapter, await readSpotBalancesFresh(spotAdapter));
|
|
377
|
+
const proceeds = Math.floor(Math.min(postQuote, postQuote - preQuote));
|
|
378
|
+
if (proceeds > 1 && await transferUsdcToPerp(spotAdapter, proceeds)) {
|
|
379
|
+
log(` [SPA] Transferred $${proceeds} (sale proceeds) back to perp`);
|
|
246
380
|
}
|
|
247
381
|
}
|
|
248
|
-
catch {
|
|
249
|
-
|
|
382
|
+
catch (err) {
|
|
383
|
+
// Non-fatal for the close (both legs are done), but surfaced: proceeds remain in spot.
|
|
384
|
+
log(` [SPA] WARNING: spot→perp transfer after close failed on ${spotExchange}: ${err instanceof Error ? err.message : String(err)}`);
|
|
385
|
+
}
|
|
386
|
+
if (perpResidual !== null) {
|
|
387
|
+
return manual(`${residualNote}; spot sold ${fill.filledSize} — the rest (${fillSizeString(Number(size) - fill.filledSize, dec)} spot vs the residual short) needs a manual check`);
|
|
388
|
+
}
|
|
389
|
+
return remainingSize === null ? { closed: true, remainingSize: null, manual: false } : { closed: false, remainingSize, manual: false };
|
|
250
390
|
}
|
|
251
391
|
catch (err) {
|
|
252
392
|
log(` [SPA] Close failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
-
return
|
|
393
|
+
return notClosed;
|
|
254
394
|
}
|
|
255
395
|
}
|
|
396
|
+
/** Base-token balance (total), read UNCACHED; a token with no balance row holds 0. Errors propagate. */
|
|
397
|
+
async baseBalance(spotAdapter, symbol) {
|
|
398
|
+
return this.baseOf(spotAdapter, await readSpotBalancesFresh(spotAdapter), symbol);
|
|
399
|
+
}
|
|
400
|
+
baseOf(spotAdapter, balances, symbol) {
|
|
401
|
+
const row = balances.find(b => spotTokenBase(b.token) === symbol.toUpperCase());
|
|
402
|
+
if (row === undefined)
|
|
403
|
+
return 0;
|
|
404
|
+
const n = parseFloat(row.total);
|
|
405
|
+
if (!Number.isFinite(n))
|
|
406
|
+
throw new Error(`${spotAdapter.name} ${symbol} balance is not a number: ${row.total}`);
|
|
407
|
+
return n;
|
|
408
|
+
}
|
|
409
|
+
/** Spot quote total (0 when shared with perp margin — nothing to move, or no quote row). */
|
|
410
|
+
quoteOf(spotExchange, spotAdapter, balances) {
|
|
411
|
+
const quoteBal = findSpotQuoteBalance(getVenue(spotExchange), spotNetworkOf(spotAdapter), balances);
|
|
412
|
+
if (!quoteBal || quoteBal.shared)
|
|
413
|
+
return 0;
|
|
414
|
+
const n = parseFloat(quoteBal.total);
|
|
415
|
+
if (!Number.isFinite(n))
|
|
416
|
+
throw new Error(`${spotExchange} spot ${quoteBal.token} balance is not a number: ${quoteBal.total}`);
|
|
417
|
+
return n;
|
|
418
|
+
}
|
|
256
419
|
/**
|
|
257
|
-
* Rotate perp short to better exchange:
|
|
420
|
+
* Rotate perp short to better exchange (spot stays unchanged):
|
|
258
421
|
* 1. Set leverage on new exchange
|
|
259
|
-
* 2. Close current perp short (reduceOnly)
|
|
260
|
-
* 3. Open new perp short on new exchange
|
|
261
|
-
* 4.
|
|
262
|
-
*
|
|
422
|
+
* 2. Close current perp short (reduceOnly, verified)
|
|
423
|
+
* 3. Open new perp short on new exchange (verified)
|
|
424
|
+
* 4. If the new short definitely failed: re-open the old short (verified)
|
|
425
|
+
* Any leg whose outcome is unknown, or a failed rollback, returns manual (quarantine):
|
|
426
|
+
* retrying could double-short or leave the spot unhedged.
|
|
263
427
|
*/
|
|
264
428
|
async rotateShort(params) {
|
|
265
429
|
const { symbol, currentExchange, newExchange, currentAdapter, newAdapter, size, leverage, log } = params;
|
|
266
430
|
const oldSym = getPerpSymbol(symbol, currentExchange);
|
|
267
431
|
const newSym = getPerpSymbol(symbol, newExchange);
|
|
432
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
268
433
|
// Set leverage on new exchange
|
|
269
434
|
try {
|
|
270
435
|
await newAdapter.setLeverage(newSym, leverage, "cross");
|
|
271
436
|
}
|
|
272
437
|
catch (err) {
|
|
273
|
-
log(` [SPA] Rotation setLeverage failed: ${
|
|
274
|
-
return false;
|
|
438
|
+
log(` [SPA] Rotation setLeverage failed: ${errText(err)}`);
|
|
439
|
+
return { ok: false, manual: false };
|
|
275
440
|
}
|
|
276
|
-
// Close old short
|
|
441
|
+
// Close old short (reduce-only, verified, one residual close)
|
|
442
|
+
const dec = sizeDecimalsOf(size);
|
|
443
|
+
let closed;
|
|
277
444
|
try {
|
|
278
|
-
await currentAdapter
|
|
279
|
-
|
|
445
|
+
const oc = await closePerpVerified(currentAdapter, currentExchange, oldSym, "buy", size);
|
|
446
|
+
closed = fillSizeString(Math.min(oc.closed, Number(size)), dec);
|
|
447
|
+
if (oc.residual > 0) {
|
|
448
|
+
logExecution({ type: "arb_close", exchange: currentExchange, symbol, side: "buy", size, status: "failed", error: `closed ${closed}, residual ${oc.residual}`, dryRun: false, meta: { rotation: true } });
|
|
449
|
+
return { ok: false, manual: true, reason: `rotation close of the ${currentExchange} short closed ${closed} of ${size}, ${oc.residual} still open${oc.note !== null ? ` (${oc.note})` : ""} — no new short placed; ${closed} spot unhedged` };
|
|
450
|
+
}
|
|
451
|
+
logExecution({ type: "arb_close", exchange: currentExchange, symbol, side: "buy", size, status: "success", dryRun: false, meta: { rotation: true, measuredClose: oc.closed } });
|
|
280
452
|
}
|
|
281
453
|
catch (err) {
|
|
282
|
-
|
|
283
|
-
|
|
454
|
+
if (isOutcomeUnknown(err)) {
|
|
455
|
+
return { ok: false, manual: true, reason: `rotation close of the ${currentExchange} short ${size} outcome unknown (${errText(err)}) — verify the ${currentExchange} position` };
|
|
456
|
+
}
|
|
457
|
+
log(` [SPA] Rotation close failed: ${errText(err)}`);
|
|
458
|
+
return { ok: false, manual: false };
|
|
284
459
|
}
|
|
285
|
-
// Open new short
|
|
460
|
+
// Open new short sized to the VERIFIED close
|
|
286
461
|
try {
|
|
287
|
-
await newAdapter
|
|
288
|
-
logExecution({ type: "arb_entry", exchange: newExchange, symbol, side: "sell", size, status: "success", dryRun: false, meta: { rotation: true } });
|
|
462
|
+
const nf = await placePerpVerified(newAdapter, newExchange, newSym, "sell", closed);
|
|
463
|
+
logExecution({ type: "arb_entry", exchange: newExchange, symbol, side: "sell", size: closed, status: "success", dryRun: false, meta: { rotation: true, measuredFill: nf.filledSize } });
|
|
464
|
+
if (nf.filledSize < Number(closed) * (1 - 1e-6) || Number(closed) < Number(size)) {
|
|
465
|
+
// Accepted partial fill (or a smaller old short): the spot is no longer fully hedged — never resubmit.
|
|
466
|
+
return { ok: false, manual: true, reason: `rotation: new ${newExchange} short filled ${nf.filledSize} of ${closed} (old ${currentExchange} short closed ${closed} of ${size}) — spot ${size} ${symbol} partly unhedged` };
|
|
467
|
+
}
|
|
289
468
|
}
|
|
290
469
|
catch (err) {
|
|
291
|
-
const msg =
|
|
470
|
+
const msg = errText(err);
|
|
471
|
+
if (isOutcomeUnknown(err)) {
|
|
472
|
+
// The new short may exist: re-opening the old one could double-short.
|
|
473
|
+
return { ok: false, manual: true, reason: `rotation: new ${newExchange} short ${closed} outcome unknown (${msg}); the ${currentExchange} short is closed — spot may be unhedged or double-hedged` };
|
|
474
|
+
}
|
|
292
475
|
log(` [SPA] Rotation new short failed: ${msg} -- rolling back`);
|
|
293
476
|
try {
|
|
294
|
-
await currentAdapter
|
|
477
|
+
const rb = await placePerpVerified(currentAdapter, currentExchange, oldSym, "sell", closed);
|
|
478
|
+
if (rb.filledSize < Number(closed) * (1 - 1e-6)) {
|
|
479
|
+
logExecution({ type: "multi_leg_rollback", exchange: currentExchange, symbol, side: "sell", size: closed, status: "failed", error: `re-opened ${rb.filledSize} of ${closed}`, dryRun: false });
|
|
480
|
+
return { ok: false, manual: true, reason: `rotation: new ${newExchange} short failed (${msg}); re-opening the ${currentExchange} short filled ${rb.filledSize} of ${closed} — spot partly unhedged` };
|
|
481
|
+
}
|
|
295
482
|
log(` [SPA] Rotation rollback OK`);
|
|
296
|
-
logExecution({ type: "multi_leg_rollback", exchange: currentExchange, symbol, side: "sell", size, status: "success", dryRun: false });
|
|
483
|
+
logExecution({ type: "multi_leg_rollback", exchange: currentExchange, symbol, side: "sell", size: closed, status: "success", dryRun: false });
|
|
297
484
|
}
|
|
298
485
|
catch (rbErr) {
|
|
299
|
-
|
|
300
|
-
logExecution({ type: "multi_leg_rollback", exchange: currentExchange, symbol, side: "sell", size, status: "failed", error:
|
|
486
|
+
const rbMsg = errText(rbErr);
|
|
487
|
+
logExecution({ type: "multi_leg_rollback", exchange: currentExchange, symbol, side: "sell", size: closed, status: "failed", error: rbMsg, dryRun: false });
|
|
488
|
+
return { ok: false, manual: true, reason: `rotation: new ${newExchange} short failed (${msg}) and re-opening the ${currentExchange} short failed or is unconfirmed (${rbMsg}) — ${closed} ${symbol} spot may be unhedged` };
|
|
301
489
|
}
|
|
302
|
-
return false;
|
|
490
|
+
return { ok: false, manual: false };
|
|
491
|
+
}
|
|
492
|
+
// The new short's entry price (arb state); a failed read is reported as unknown.
|
|
493
|
+
let entryPrice = null;
|
|
494
|
+
try {
|
|
495
|
+
invalidateCache("acct");
|
|
496
|
+
const p = (await newAdapter.getPositions()).find(x => matchSymbol(x.symbol, newSym) && parseFloat(x.size) !== 0);
|
|
497
|
+
const px = Number(p?.entryPrice);
|
|
498
|
+
entryPrice = Number.isFinite(px) && px > 0 ? px : null;
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
log(` [SPA] Rotation: ${newExchange} entry price unreadable (${errText(err)})`);
|
|
303
502
|
}
|
|
304
|
-
return true;
|
|
503
|
+
return { ok: true, entryPrice };
|
|
305
504
|
}
|
|
306
505
|
}
|