perp-cli 0.12.17 → 0.14.1
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 +303 -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 +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- 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 +357 -459
- 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 +240 -6
- package/dist/exchanges/aster.js +1139 -195
- 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 +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
- package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- 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 +1386 -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 +29 -0
- package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
- package/dist/exchanges/venues/qfex.js +140 -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 +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -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 +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- 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
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
* Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).
|
|
8
8
|
* Execution layer (SpotPerpExecutor) handles ALL exchange interactions.
|
|
9
9
|
*/
|
|
10
|
+
import { requiredArbMargin } from "../../rebalance.js";
|
|
10
11
|
import { registerStrategy } from "../strategy-registry.js";
|
|
11
12
|
import { getFundingHours } from "../../funding.js";
|
|
12
13
|
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
13
14
|
import { pruneExecutionLog } from "../../execution-log.js";
|
|
14
|
-
import { buildAdapterMap,
|
|
15
|
+
import { buildAdapterMap, getPerpSymbol, findRate, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols } from "./funding-arb-utils.js";
|
|
15
16
|
import { SpotPerpExecutor } from "./spot-perp-executor.js";
|
|
17
|
+
import { findSpotQuoteBalance, getBotSpotAdapter, nonQuoteSpotBalances, spotNetworkOf, spotSharesPerpMargin, spotTokenBase } from "../../exchanges/spot-factory.js";
|
|
18
|
+
import { getVenue } from "../../exchanges/venue.js";
|
|
19
|
+
import { updatePositionEntry } from "../../arb/state.js";
|
|
20
|
+
import { recordSpotPerpClose, recordSpotPerpEntry, stateEntryOf, stateWriteFailed } from "./spot-perp-state.js";
|
|
21
|
+
import { isQuarantined, quarantineSymbol } from "../quarantine.js";
|
|
22
|
+
/** Consecutive failed closes before a position is quarantined (no sell→fail→re-hedge loop). */
|
|
23
|
+
const MAX_CLOSE_FAILURES = 3;
|
|
24
|
+
/** Spot leg buying power: notional plus 1% for the taker fee and the spot buy's slippage bound. */
|
|
25
|
+
const SPOT_FEE_BUFFER = 1.01;
|
|
16
26
|
let _posIdCounter = 0;
|
|
17
27
|
function genPosId() { return `spa-${Date.now()}-${++_posIdCounter}`; }
|
|
18
28
|
// ── Strategy ──
|
|
@@ -38,6 +48,12 @@ export class SpotPerpArbStrategy {
|
|
|
38
48
|
}
|
|
39
49
|
_config = {};
|
|
40
50
|
_failCooldown = new Map();
|
|
51
|
+
/** Consecutive failed close attempts per symbol (quarantined at MAX_CLOSE_FAILURES). */
|
|
52
|
+
_closeFailures = new Map();
|
|
53
|
+
/** Venues whose positions could not be read during recovery: no entries there until re-read. */
|
|
54
|
+
_unrecovered = new Set();
|
|
55
|
+
/** Perp symbols this bot entered or tracked this run — the only symbols its stop cancels orders on. */
|
|
56
|
+
_traded = new Set();
|
|
41
57
|
_fundingScoreCache = new Map();
|
|
42
58
|
get params() {
|
|
43
59
|
const c = this._config;
|
|
@@ -57,6 +73,7 @@ export class SpotPerpArbStrategy {
|
|
|
57
73
|
}
|
|
58
74
|
// ── init ──
|
|
59
75
|
async init(ctx, _snapshot) {
|
|
76
|
+
assertFundingArbLegs(ctx);
|
|
60
77
|
try {
|
|
61
78
|
pruneExecutionLog(30);
|
|
62
79
|
}
|
|
@@ -73,7 +90,8 @@ export class SpotPerpArbStrategy {
|
|
|
73
90
|
ctx.log(` [SPA] Exchanges: ${p.exchanges.join(", ")}`);
|
|
74
91
|
try {
|
|
75
92
|
const adapters = buildAdapterMap(ctx);
|
|
76
|
-
const recovered = await this.recoverPositions(adapters, ctx);
|
|
93
|
+
const { recovered, unreadable } = await this.recoverPositions(adapters, ctx);
|
|
94
|
+
this._unrecovered = unreadable;
|
|
77
95
|
if (recovered.length > 0) {
|
|
78
96
|
ctx.state.set("spaPositions", recovered);
|
|
79
97
|
ctx.state.set("arbPositions", recovered.length);
|
|
@@ -105,6 +123,19 @@ export class SpotPerpArbStrategy {
|
|
|
105
123
|
const p = this.params;
|
|
106
124
|
const positions = ctx.state.get("spaPositions");
|
|
107
125
|
const adapters = buildAdapterMap(ctx);
|
|
126
|
+
// Retry recovery on venues whose positions could not be read at init.
|
|
127
|
+
if (this._unrecovered.size > 0) {
|
|
128
|
+
const { recovered, unreadable } = await this.recoverPositions(adapters, ctx);
|
|
129
|
+
const tracked = new Set(positions.map(pos => pos.symbol.toUpperCase()));
|
|
130
|
+
for (const r of recovered) {
|
|
131
|
+
if (tracked.has(r.symbol.toUpperCase()))
|
|
132
|
+
continue;
|
|
133
|
+
positions.push(r);
|
|
134
|
+
tracked.add(r.symbol.toUpperCase());
|
|
135
|
+
}
|
|
136
|
+
ctx.state.set("arbPositions", positions.length);
|
|
137
|
+
this._unrecovered = unreadable;
|
|
138
|
+
}
|
|
108
139
|
const exchangeBalances = new Map();
|
|
109
140
|
const availableAdapters = new Map();
|
|
110
141
|
for (const [name, a] of adapters) {
|
|
@@ -115,7 +146,9 @@ export class SpotPerpArbStrategy {
|
|
|
115
146
|
const available = parseFloat(bal.available);
|
|
116
147
|
exchangeBalances.set(name, { equity, available, marginPct });
|
|
117
148
|
const cd = this._failCooldown.get(`${name}:exchange`) ?? 0;
|
|
118
|
-
if (
|
|
149
|
+
if (this._unrecovered.has(name))
|
|
150
|
+
ctx.log(` [SPA] ${name} positions not recovered — no entries this tick`);
|
|
151
|
+
else if (Date.now() < cd)
|
|
119
152
|
ctx.log(` [SPA] ${name} on cooldown`);
|
|
120
153
|
else if (available < 5)
|
|
121
154
|
ctx.log(` [SPA] ${name} available $${available.toFixed(2)} too low`);
|
|
@@ -131,7 +164,15 @@ export class SpotPerpArbStrategy {
|
|
|
131
164
|
}
|
|
132
165
|
const ratesByExchange = new Map();
|
|
133
166
|
for (const [name, a] of adapters) {
|
|
134
|
-
|
|
167
|
+
let rates;
|
|
168
|
+
try {
|
|
169
|
+
rates = await fetchRates(a, name);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
// Recorded per venue; this venue is left out of this cycle's rate map.
|
|
173
|
+
ctx.log(` [SPA] fetchRates failed for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
135
176
|
const map = new Map();
|
|
136
177
|
for (const r of rates)
|
|
137
178
|
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage, fundingHours: r.fundingHours });
|
|
@@ -189,6 +230,10 @@ export class SpotPerpArbStrategy {
|
|
|
189
230
|
for (const pos of positions) {
|
|
190
231
|
if (!this.shouldClose(pos, ratesByExchange, p))
|
|
191
232
|
continue;
|
|
233
|
+
if (this.quarantined(pos.symbol, ctx)) {
|
|
234
|
+
ctx.log(` [SPA] ${pos.symbol} is quarantined — not closing (perp strategy quarantine)`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
192
237
|
const cd = this._failCooldown.get(`${pos.symbol}:close`) ?? 0;
|
|
193
238
|
if (Date.now() < cd)
|
|
194
239
|
continue;
|
|
@@ -206,23 +251,67 @@ export class SpotPerpArbStrategy {
|
|
|
206
251
|
symbol: pos.symbol, spotExchange: pos.spotExchange, perpExchange: pos.perpExchange,
|
|
207
252
|
spotAdapter, perpAdapter, size: pos.size, log: ctx.log,
|
|
208
253
|
});
|
|
209
|
-
if (closed)
|
|
254
|
+
if (closed.closed) {
|
|
210
255
|
toClose.push(pos);
|
|
211
|
-
|
|
256
|
+
this._closeFailures.delete(pos.symbol);
|
|
257
|
+
this.recordClose(spotAdapter, pos, null, ctx);
|
|
258
|
+
}
|
|
259
|
+
else if (closed.manual) {
|
|
260
|
+
this.quarantine(pos.symbol, closed.reason, pos.spotExchange, pos.perpExchange, ctx);
|
|
261
|
+
}
|
|
262
|
+
else if (closed.remainingSize !== null) {
|
|
263
|
+
pos.size = closed.remainingSize; // partial close: remainder still hedged
|
|
264
|
+
this.recordClose(spotAdapter, pos, closed.remainingSize, ctx);
|
|
265
|
+
this._closeFailures.delete(pos.symbol);
|
|
212
266
|
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Each failed attempt may have traded (perp close → spot sell fail → re-hedge):
|
|
270
|
+
// stop after MAX_CLOSE_FAILURES instead of looping.
|
|
271
|
+
const n = (this._closeFailures.get(pos.symbol) ?? 0) + 1;
|
|
272
|
+
this._closeFailures.set(pos.symbol, n);
|
|
273
|
+
if (n >= MAX_CLOSE_FAILURES) {
|
|
274
|
+
this.quarantine(pos.symbol, `close failed ${n} times in a row — stopping automatic close attempts`, pos.spotExchange, pos.perpExchange, ctx);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
213
280
|
}
|
|
214
281
|
if (toClose.length > 0) {
|
|
215
282
|
const remaining = positions.filter(pos => !toClose.some(c => c.id === pos.id));
|
|
216
283
|
ctx.state.set("spaPositions", remaining);
|
|
217
284
|
ctx.state.set("arbPositions", remaining.length);
|
|
218
285
|
}
|
|
219
|
-
// ── Rotation check ──
|
|
286
|
+
// ── Rotation check (only to perp venues that match the spot leg, §12.4 #6) ──
|
|
287
|
+
const perpMarketsCache = new Map();
|
|
288
|
+
const allowedBySpotVenue = new Map();
|
|
220
289
|
const currentPositions = ctx.state.get("spaPositions");
|
|
221
290
|
for (const pos of currentPositions) {
|
|
291
|
+
if (this.quarantined(pos.symbol, ctx))
|
|
292
|
+
continue;
|
|
222
293
|
const rcd = this._failCooldown.get(`${pos.id}:rotate`) ?? 0;
|
|
223
294
|
if (Date.now() < rcd)
|
|
224
295
|
continue;
|
|
225
|
-
|
|
296
|
+
let allowedByBase = allowedBySpotVenue.get(pos.spotExchange);
|
|
297
|
+
if (allowedByBase === undefined) {
|
|
298
|
+
const spotExAdapter = adapters.get(pos.spotExchange);
|
|
299
|
+
const rotSpot = spotExAdapter ? await getBotSpotAdapter(pos.spotExchange, spotExAdapter) : null;
|
|
300
|
+
if (rotSpot === null)
|
|
301
|
+
continue;
|
|
302
|
+
try {
|
|
303
|
+
allowedByBase = await this.spotPerpVenues(pos.spotExchange, await rotSpot.getSpotMarkets(), adapters, perpMarketsCache, ctx);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
ctx.log(` [SPA] ${pos.spotExchange} spot↔perp matching failed (rotation skipped): ${e instanceof Error ? e.message : String(e)}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
allowedBySpotVenue.set(pos.spotExchange, allowedByBase);
|
|
310
|
+
}
|
|
311
|
+
const allowed = allowedByBase.get(pos.symbol.toUpperCase());
|
|
312
|
+
if (allowed === undefined)
|
|
313
|
+
continue;
|
|
314
|
+
const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange, allowed);
|
|
226
315
|
const best = candidates[0];
|
|
227
316
|
if (!best || best.exchange === pos.perpExchange)
|
|
228
317
|
continue;
|
|
@@ -247,11 +336,26 @@ export class SpotPerpArbStrategy {
|
|
|
247
336
|
currentAdapter: oldAdapter, newAdapter,
|
|
248
337
|
size: pos.size, leverage: Math.min(p.leverage, newMaxLev), log: ctx.log,
|
|
249
338
|
});
|
|
250
|
-
if (ok) {
|
|
339
|
+
if (ok.ok) {
|
|
340
|
+
// The spot leg is unchanged: the state entry follows the short to its new venue.
|
|
341
|
+
try {
|
|
342
|
+
const spotExAdapter = adapters.get(pos.spotExchange);
|
|
343
|
+
const rotSpot = spotExAdapter ? await this.executor.getSpotAdapter(pos.spotExchange, spotExAdapter) : null;
|
|
344
|
+
const entry = rotSpot !== null ? stateEntryOf(rotSpot, pos) : undefined;
|
|
345
|
+
if (rotSpot !== null && entry !== undefined)
|
|
346
|
+
updatePositionEntry(spotNetworkOf(rotSpot), entry, { shortExchange: getVenue(best.exchange).id, entryShortPrice: ok.entryPrice });
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
stateWriteFailed("[SPA]", "update (rotation)", { ...pos, perpExchange: best.exchange }, e, ctx.log);
|
|
350
|
+
}
|
|
251
351
|
pos.perpExchange = best.exchange;
|
|
252
352
|
pos.fundingHistory = [];
|
|
253
353
|
pos.lastFundingCheck = 0;
|
|
254
354
|
}
|
|
355
|
+
else if (ok.manual) {
|
|
356
|
+
// A leg is unknown / unhedged: a retry could double-short — quarantine, never cool down.
|
|
357
|
+
this.quarantine(pos.symbol, ok.reason, pos.spotExchange, `${pos.perpExchange}/${best.exchange}`, ctx);
|
|
358
|
+
}
|
|
255
359
|
else {
|
|
256
360
|
this._failCooldown.set(`${pos.id}:rotate`, Date.now() + 10 * 60 * 1000);
|
|
257
361
|
}
|
|
@@ -266,12 +370,12 @@ export class SpotPerpArbStrategy {
|
|
|
266
370
|
const HL_SPOT_WHITELIST = new Set(["UBTC", "UETH", "USOL", "UFART", "UPUMP", "LINK0", "AVAX0", "AAVE0", "PURR", "HYPE"]);
|
|
267
371
|
for (const [spotExName, spotExAdapter] of adapters) {
|
|
268
372
|
try {
|
|
269
|
-
const spotAdapter = await
|
|
373
|
+
const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
|
|
270
374
|
if (!spotAdapter)
|
|
271
375
|
continue;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
376
|
+
// Canonical symbol + price guard (§12.4 #6): base → perp venues it may hedge on.
|
|
377
|
+
const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
|
|
378
|
+
for (const [base, allowed] of allowedByBase) {
|
|
275
379
|
// HL spot: skip tokens not in whitelist
|
|
276
380
|
if (spotExName === "hyperliquid" && !HL_SPOT_WHITELIST.has(base))
|
|
277
381
|
continue;
|
|
@@ -279,7 +383,9 @@ export class SpotPerpArbStrategy {
|
|
|
279
383
|
continue;
|
|
280
384
|
if (Date.now() < (this._failCooldown.get(`${base}:entry`) ?? 0))
|
|
281
385
|
continue;
|
|
282
|
-
|
|
386
|
+
if (this.quarantined(base, ctx))
|
|
387
|
+
continue;
|
|
388
|
+
const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange, allowed);
|
|
283
389
|
for (const cand of candidates) {
|
|
284
390
|
if (cand.score.annualized < p.min_rate || cand.score.consistency < p.min_consistency)
|
|
285
391
|
continue;
|
|
@@ -287,7 +393,9 @@ export class SpotPerpArbStrategy {
|
|
|
287
393
|
}
|
|
288
394
|
}
|
|
289
395
|
}
|
|
290
|
-
catch {
|
|
396
|
+
catch (e) {
|
|
397
|
+
ctx.log(` [SPA] ${spotExName} spot scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
398
|
+
}
|
|
291
399
|
}
|
|
292
400
|
scoredOpps.sort((a, b) => b.annualized - a.annualized);
|
|
293
401
|
if (scoredOpps.length > 0 && numPos < p.max_positions) {
|
|
@@ -298,6 +406,8 @@ export class SpotPerpArbStrategy {
|
|
|
298
406
|
for (const opp of scoredOpps) {
|
|
299
407
|
if (ctx.state.get("spaPositions").length >= p.max_positions)
|
|
300
408
|
break;
|
|
409
|
+
if (this._unrecovered.has(opp.spotExchange) || this._unrecovered.has(opp.perpExchange))
|
|
410
|
+
continue;
|
|
301
411
|
const opened = await this.tryOpenPosition(opp.spotExchange, opp.perpExchange, opp.symbol, opp.annualized, adapters, ctx, ratesByExchange, exchangeBalances);
|
|
302
412
|
if (opened)
|
|
303
413
|
return [{ type: "noop" }];
|
|
@@ -311,9 +421,16 @@ export class SpotPerpArbStrategy {
|
|
|
311
421
|
async onStop(ctx) {
|
|
312
422
|
const positions = ctx.state.get("spaPositions");
|
|
313
423
|
const adapters = buildAdapterMap(ctx);
|
|
424
|
+
for (const p of positions ?? [])
|
|
425
|
+
this._traded.add(getPerpSymbol(p.symbol, p.perpExchange).toUpperCase());
|
|
314
426
|
if (positions && positions.length > 0) {
|
|
315
427
|
const remaining = [];
|
|
316
428
|
for (const pos of positions) {
|
|
429
|
+
if (this.quarantined(pos.symbol, ctx)) {
|
|
430
|
+
ctx.log(` [SPA] CRITICAL: ${pos.symbol} is quarantined — left open on stop; resolve manually (perp strategy quarantine)`);
|
|
431
|
+
remaining.push(pos);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
317
434
|
const spotExAdapter = adapters.get(pos.spotExchange) ?? adapters.get(pos.perpExchange);
|
|
318
435
|
const perpAdapter = adapters.get(pos.perpExchange);
|
|
319
436
|
if (!perpAdapter || !spotExAdapter) {
|
|
@@ -331,33 +448,62 @@ export class SpotPerpArbStrategy {
|
|
|
331
448
|
symbol: pos.symbol, spotExchange: pos.spotExchange, perpExchange: pos.perpExchange,
|
|
332
449
|
spotAdapter, perpAdapter, size: pos.size, log: ctx.log,
|
|
333
450
|
});
|
|
334
|
-
if (
|
|
335
|
-
|
|
451
|
+
if (closed.closed)
|
|
452
|
+
this.recordClose(spotAdapter, pos, null, ctx);
|
|
453
|
+
else {
|
|
454
|
+
if (closed.manual)
|
|
455
|
+
this.quarantine(pos.symbol, closed.reason, pos.spotExchange, pos.perpExchange, ctx);
|
|
456
|
+
else if (closed.remainingSize !== null) {
|
|
457
|
+
pos.size = closed.remainingSize; // partial close: remainder still hedged
|
|
458
|
+
this.recordClose(spotAdapter, pos, closed.remainingSize, ctx);
|
|
459
|
+
}
|
|
460
|
+
ctx.log(` [SPA] CRITICAL: ${pos.symbol} could not be (fully) closed on stop`);
|
|
336
461
|
remaining.push(pos);
|
|
337
462
|
}
|
|
338
463
|
}
|
|
339
464
|
ctx.state.set("spaPositions", remaining);
|
|
340
465
|
ctx.state.set("arbPositions", remaining.length);
|
|
341
466
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return
|
|
467
|
+
// Only the bot's own symbols, on each venue it used — never an account-wide cancel.
|
|
468
|
+
await cancelBotSymbols(adapters, this._traded, "[SPA]", ctx.log);
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
/** Close recorded in the arb state: entry removed (remaining null) or its legs set to the still-hedged remainder. */
|
|
472
|
+
recordClose(spot, pos, remaining, ctx) {
|
|
473
|
+
recordSpotPerpClose(spot, pos, remaining, "[SPA]", ctx.log);
|
|
474
|
+
}
|
|
475
|
+
quarantined(symbol, ctx) {
|
|
476
|
+
return isQuarantined(symbol, ctx.log);
|
|
477
|
+
}
|
|
478
|
+
/** Unknown / unhedged outcome (executor `manual`): stop touching this symbol until a human clears it. */
|
|
479
|
+
quarantine(symbol, reason, spotExchange, perpExchange, ctx) {
|
|
480
|
+
quarantineSymbol(this.name, symbol, reason, `Check ${spotExchange} spot (perp -e ${spotExchange} trade spot-balance, perp -e ${spotExchange} trade spot-orders) and the ${perpExchange} perp (perp -e ${perpExchange} positions); resolve by hand, then: perp strategy quarantine-clear ${symbol.toUpperCase()}`, ctx.log);
|
|
352
481
|
}
|
|
353
482
|
// ── Scoring (decision logic) ──
|
|
354
483
|
scoreFundingLocal(adapter, symbol, exchangeName) {
|
|
355
484
|
return scoreFunding(adapter, symbol, exchangeName, this.params.history_periods, this._fundingScoreCache);
|
|
356
485
|
}
|
|
486
|
+
/** base (upper-case) → perp venues it may hedge on (canonical symbol + price guard, `botSpotPerpMatches`). */
|
|
487
|
+
async spotPerpVenues(spotExName, spotMarkets, adapters, perpMarketsCache, ctx) {
|
|
488
|
+
const byPerp = await botSpotPerpMatches(spotExName, spotMarkets, [...adapters.keys()], perpMarketsCache, (msg) => ctx.log(` [SPA] ${msg}`));
|
|
489
|
+
const byBase = new Map();
|
|
490
|
+
for (const [perpVenue, bases] of byPerp) {
|
|
491
|
+
for (const b of bases) {
|
|
492
|
+
const set = byBase.get(b);
|
|
493
|
+
if (set)
|
|
494
|
+
set.add(perpVenue);
|
|
495
|
+
else
|
|
496
|
+
byBase.set(b, new Set([perpVenue]));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return byBase;
|
|
500
|
+
}
|
|
357
501
|
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
|
|
358
|
-
async findPerpCandidates(symbol, adapters, ratesByExchange) {
|
|
502
|
+
async findPerpCandidates(symbol, adapters, ratesByExchange, allowedPerpVenues) {
|
|
359
503
|
const candidates = [];
|
|
360
504
|
for (const [exName, adapter] of adapters) {
|
|
505
|
+
if (!allowedPerpVenues.has(exName))
|
|
506
|
+
continue;
|
|
361
507
|
const perpSym = getPerpSymbol(symbol, exName);
|
|
362
508
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
363
509
|
if (!ri || ri.rate <= 0)
|
|
@@ -403,6 +549,11 @@ export class SpotPerpArbStrategy {
|
|
|
403
549
|
// ── Open position (decision + sizing, delegates execution) ──
|
|
404
550
|
async tryOpenPosition(spotExchange, perpExchange, symbol, entryRate, adapters, ctx, ratesByExchange, exchangeBalances) {
|
|
405
551
|
const p = this.params;
|
|
552
|
+
// Engine: equity unknown → no new entries (closes / rotations' closes still run).
|
|
553
|
+
if (ctx.state.get("equityUnknown") === true) {
|
|
554
|
+
ctx.log(` [SPA] Skip ${symbol}: equity unknown — no new entries`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
406
557
|
const spotExAdapter = adapters.get(spotExchange);
|
|
407
558
|
const perpAdapter = adapters.get(perpExchange);
|
|
408
559
|
if (!spotExAdapter || !perpAdapter) {
|
|
@@ -455,11 +606,12 @@ export class SpotPerpArbStrategy {
|
|
|
455
606
|
if (sameExchange) {
|
|
456
607
|
const perpBal = await perpAdapter.getBalance();
|
|
457
608
|
const perpAvail = parseFloat(perpBal.available);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
609
|
+
// Spot leg: notional × SPOT_FEE_BUFFER; perp leg: requiredArbMargin (notional / leverage × ARB_MARGIN_BUFFER).
|
|
610
|
+
const need = (n) => n * SPOT_FEE_BUFFER + requiredArbMargin(n, leverage);
|
|
611
|
+
if (perpAvail < need(targetSizeUsd)) {
|
|
612
|
+
const max = perpAvail / need(1);
|
|
461
613
|
if (max < 10) {
|
|
462
|
-
ctx.log(` [SPA] Skip ${symbol}: insufficient balance`);
|
|
614
|
+
ctx.log(` [SPA] Skip ${symbol}: insufficient balance — $${perpAvail.toFixed(2)} available, $${need(10).toFixed(2)} needed for the $10 minimum (spot + perp margin at ${leverage}x)`);
|
|
463
615
|
return false;
|
|
464
616
|
}
|
|
465
617
|
targetSizeUsd = Math.floor(max);
|
|
@@ -473,38 +625,45 @@ export class SpotPerpArbStrategy {
|
|
|
473
625
|
return false;
|
|
474
626
|
// Check spot USDC balance (only add if separate from perp — skip for unified accounts like HL)
|
|
475
627
|
let spotAvailable = sBal.available;
|
|
476
|
-
const
|
|
477
|
-
if (!
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (spotUsdc)
|
|
482
|
-
spotAvailable += parseFloat(spotUsdc.available);
|
|
483
|
-
}
|
|
484
|
-
catch { /* non-critical */ }
|
|
628
|
+
const spotDef = getVenue(spotExchange);
|
|
629
|
+
if (!spotSharesPerpMargin(spotDef, spotExAdapter)) {
|
|
630
|
+
const spotQuote = findSpotQuoteBalance(spotDef, spotNetworkOf(spotAdapter), await spotAdapter.getSpotBalances());
|
|
631
|
+
if (spotQuote)
|
|
632
|
+
spotAvailable += parseFloat(spotQuote.available);
|
|
485
633
|
}
|
|
486
|
-
|
|
634
|
+
// Spot leg: notional × SPOT_FEE_BUFFER of spot buying power; perp leg: requiredArbMargin at the leverage.
|
|
635
|
+
const maxNtl = Math.min(spotAvailable / SPOT_FEE_BUFFER, pBal.available / requiredArbMargin(1, leverage));
|
|
487
636
|
if (maxNtl < targetSizeUsd) {
|
|
488
637
|
if (maxNtl < 10) {
|
|
489
|
-
ctx.log(` [SPA] Skip ${symbol}: insufficient balance
|
|
638
|
+
ctx.log(` [SPA] Skip ${symbol}: insufficient balance for the $10 minimum — spot $${spotAvailable.toFixed(2)} (needs $${(10 * SPOT_FEE_BUFFER).toFixed(2)}), perp $${pBal.available.toFixed(2)} (needs $${requiredArbMargin(10, leverage).toFixed(2)} margin at ${leverage}x)`);
|
|
490
639
|
return false;
|
|
491
640
|
}
|
|
492
641
|
targetSizeUsd = Math.floor(maxNtl);
|
|
493
642
|
}
|
|
494
643
|
}
|
|
495
644
|
// Delegate to executor
|
|
645
|
+
this._traded.add(perpSymbol.toUpperCase());
|
|
496
646
|
const result = await this.executor.openPosition({
|
|
497
647
|
symbol, perpSymbol, spotExchange, perpExchange,
|
|
498
648
|
spotAdapter, perpAdapter, sizeUsd: targetSizeUsd, leverage, log: ctx.log,
|
|
499
649
|
});
|
|
500
650
|
if (result.success) {
|
|
501
651
|
const cur = ctx.state.get("spaPositions");
|
|
502
|
-
|
|
652
|
+
const pos = { id: genPosId(), symbol, spotExchange, perpExchange, size: result.size, entryTime: Date.now(), entryRate, fundingHistory: [], lastFundingCheck: 0 };
|
|
653
|
+
cur.push(pos);
|
|
654
|
+
recordSpotPerpEntry(spotAdapter, {
|
|
655
|
+
id: pos.id, symbol, spotExchange, perpExchange, entryTime: pos.entryTime, entrySpread: entryRate,
|
|
656
|
+
spotSize: result.spotSize, perpSize: Number(result.size), spotAvgPrice: result.spotAvgPrice, perpEntryPrice: result.perpEntryPrice,
|
|
657
|
+
}, "[SPA]", ctx.log);
|
|
503
658
|
ctx.state.set("spaPositions", cur);
|
|
504
659
|
ctx.state.set("arbPositions", cur.length);
|
|
505
660
|
ctx.log(` [SPA] Opened! (${cur.length}/${p.max_positions})`);
|
|
506
661
|
return true;
|
|
507
662
|
}
|
|
663
|
+
if (result.manual === true) {
|
|
664
|
+
this.quarantine(symbol, result.error ?? "entry outcome unknown", spotExchange, perpExchange, ctx);
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
508
667
|
this._failCooldown.set(`${symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
509
668
|
return false;
|
|
510
669
|
}
|
|
@@ -515,25 +674,30 @@ export class SpotPerpArbStrategy {
|
|
|
515
674
|
}
|
|
516
675
|
}
|
|
517
676
|
// ── Position recovery ──
|
|
677
|
+
/** Rebuild positions from the venues; `unreadable` = venues whose positions / spot balances failed to load (never "none"). */
|
|
518
678
|
async recoverPositions(adapters, ctx) {
|
|
519
679
|
const recovered = [];
|
|
680
|
+
const unreadable = new Set();
|
|
520
681
|
const perpByEx = new Map();
|
|
521
682
|
for (const [name, a] of adapters) {
|
|
522
683
|
try {
|
|
523
684
|
const pos = await a.getPositions();
|
|
524
685
|
perpByEx.set(name, pos.map(p => ({ symbol: p.symbol.toUpperCase(), side: p.side, size: p.size })));
|
|
525
686
|
}
|
|
526
|
-
catch {
|
|
687
|
+
catch (e) {
|
|
688
|
+
unreadable.add(name);
|
|
689
|
+
ctx.log(` [SPA] ${name} positions unreadable during recovery (${e instanceof Error ? e.message : String(e)}) — no entries on ${name} until recovered`);
|
|
690
|
+
}
|
|
527
691
|
}
|
|
528
692
|
const used = new Set();
|
|
529
693
|
for (const [spotExName, spotExAdapter] of adapters) {
|
|
530
694
|
try {
|
|
531
|
-
const spotAdapter = await
|
|
695
|
+
const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
|
|
532
696
|
if (!spotAdapter)
|
|
533
697
|
continue;
|
|
534
698
|
const bals = await spotAdapter.getSpotBalances();
|
|
535
|
-
for (const bal of
|
|
536
|
-
const base = bal.token
|
|
699
|
+
for (const bal of nonQuoteSpotBalances(getVenue(spotExName), spotNetworkOf(spotAdapter), bals)) {
|
|
700
|
+
const base = spotTokenBase(bal.token);
|
|
537
701
|
for (const [perpExName, perps] of perpByEx) {
|
|
538
702
|
for (const perp of perps) {
|
|
539
703
|
const key = `${perpExName}:${perp.symbol}`;
|
|
@@ -549,9 +713,12 @@ export class SpotPerpArbStrategy {
|
|
|
549
713
|
}
|
|
550
714
|
}
|
|
551
715
|
}
|
|
552
|
-
catch {
|
|
716
|
+
catch (e) {
|
|
717
|
+
unreadable.add(spotExName);
|
|
718
|
+
ctx.log(` [SPA] ${spotExName} spot recovery failed: ${e instanceof Error ? e.message : String(e)} — no entries on ${spotExName} until recovered`);
|
|
719
|
+
}
|
|
553
720
|
}
|
|
554
|
-
return recovered;
|
|
721
|
+
return { recovered, unreadable };
|
|
555
722
|
}
|
|
556
723
|
}
|
|
557
724
|
registerStrategy("spot-perp-arb", (_config) => new SpotPerpArbStrategy());
|
|
@@ -6,20 +6,63 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ExchangeAdapter } from "../../exchanges/index.js";
|
|
8
8
|
import type { SpotAdapter } from "../../exchanges/spot-interface.js";
|
|
9
|
-
export
|
|
10
|
-
success:
|
|
11
|
-
size
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
export type ExecutionResult = {
|
|
10
|
+
success: true;
|
|
11
|
+
/** Perp short size (= the hedged spot, on the perp lot grid). */
|
|
12
|
+
size: string;
|
|
13
|
+
notional: number;
|
|
14
|
+
/** Spot base units bought (the verified fill). */
|
|
15
|
+
spotSize: number;
|
|
16
|
+
/** The spot fill's average price; null = the venue reported none. */
|
|
17
|
+
spotAvgPrice: number | null;
|
|
18
|
+
/** The venue's entry price of the perp short after the hedge; null = not readable. */
|
|
19
|
+
perpEntryPrice: number | null;
|
|
20
|
+
} | {
|
|
21
|
+
success: false;
|
|
22
|
+
error: string;
|
|
23
|
+
/** true = outcome unknown or a leg left unhedged: the caller must quarantine the symbol (no retry). */
|
|
24
|
+
manual?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
|
|
28
|
+
* manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
|
|
29
|
+
* the position and never retry it automatically (design §12.3).
|
|
30
|
+
*/
|
|
31
|
+
export type CloseResult = {
|
|
32
|
+
closed: true;
|
|
33
|
+
remainingSize: null;
|
|
34
|
+
manual: false;
|
|
35
|
+
} | {
|
|
36
|
+
closed: false;
|
|
37
|
+
remainingSize: string | null;
|
|
38
|
+
manual: false;
|
|
39
|
+
} | {
|
|
40
|
+
closed: false;
|
|
41
|
+
remainingSize: null;
|
|
42
|
+
manual: true;
|
|
43
|
+
reason: string;
|
|
44
|
+
};
|
|
45
|
+
/** Rotation outcome (ok: the venue's entry price of the new short, null = not readable); manual = a leg's state is unknown or unhedged: the caller must quarantine. */
|
|
46
|
+
export type RotationResult = {
|
|
47
|
+
ok: true;
|
|
48
|
+
entryPrice: number | null;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
manual: false;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
manual: true;
|
|
55
|
+
reason: string;
|
|
56
|
+
};
|
|
15
57
|
export declare class SpotPerpExecutor {
|
|
16
|
-
|
|
17
|
-
/** Get or create cached spot adapter. */
|
|
58
|
+
/** Spot adapter for a bot spot venue (factory-cached); null outside the bot spot scope. */
|
|
18
59
|
getSpotAdapter(name: string, adapter: ExchangeAdapter): Promise<SpotAdapter | null>;
|
|
19
|
-
/** Transfer
|
|
20
|
-
transferToSpot(spotAdapter: SpotAdapter,
|
|
21
|
-
/** Transfer
|
|
22
|
-
transferToPerp(spotAdapter: SpotAdapter,
|
|
60
|
+
/** Transfer the spot quote asset perp -> spot. false = shared margin (nothing moved). */
|
|
61
|
+
transferToSpot(spotAdapter: SpotAdapter, amount: number): Promise<boolean>;
|
|
62
|
+
/** Transfer the spot quote asset spot -> perp. false = shared margin (nothing moved). */
|
|
63
|
+
transferToPerp(spotAdapter: SpotAdapter, amount: number): Promise<boolean>;
|
|
64
|
+
/** Rollback transfer spot → perp: failure is logged loudly (funds remain in spot), never swallowed. */
|
|
65
|
+
private returnToPerp;
|
|
23
66
|
/**
|
|
24
67
|
* Full spot-perp entry flow:
|
|
25
68
|
* 1. Check liquidity
|
|
@@ -47,6 +90,11 @@ export declare class SpotPerpExecutor {
|
|
|
47
90
|
* 1. Close perp short FIRST (reduceOnly) — hedge stays intact if this fails
|
|
48
91
|
* 2. Sell spot
|
|
49
92
|
* 3. Transfer USDC back to perp (always, not just same-exchange)
|
|
93
|
+
*
|
|
94
|
+
* Returns `manual: true` (caller quarantines, never retries) when the spot outcome is
|
|
95
|
+
* unknown (pending sell, failed post-trade verification) or spot is left unhedged.
|
|
96
|
+
* A perp short is re-opened only when THIS call closed it; if the perp was already
|
|
97
|
+
* gone and spot cannot be sold, nothing new is opened — the position is quarantined.
|
|
50
98
|
*/
|
|
51
99
|
closePosition(params: {
|
|
52
100
|
symbol: string;
|
|
@@ -56,14 +104,20 @@ export declare class SpotPerpExecutor {
|
|
|
56
104
|
perpAdapter: ExchangeAdapter;
|
|
57
105
|
size: string;
|
|
58
106
|
log: (msg: string) => void;
|
|
59
|
-
}): Promise<
|
|
107
|
+
}): Promise<CloseResult>;
|
|
108
|
+
/** Base-token balance (total), read UNCACHED; a token with no balance row holds 0. Errors propagate. */
|
|
109
|
+
private baseBalance;
|
|
110
|
+
private baseOf;
|
|
111
|
+
/** Spot quote total (0 when shared with perp margin — nothing to move, or no quote row). */
|
|
112
|
+
private quoteOf;
|
|
60
113
|
/**
|
|
61
|
-
* Rotate perp short to better exchange:
|
|
114
|
+
* Rotate perp short to better exchange (spot stays unchanged):
|
|
62
115
|
* 1. Set leverage on new exchange
|
|
63
|
-
* 2. Close current perp short (reduceOnly)
|
|
64
|
-
* 3. Open new perp short on new exchange
|
|
65
|
-
* 4.
|
|
66
|
-
*
|
|
116
|
+
* 2. Close current perp short (reduceOnly, verified)
|
|
117
|
+
* 3. Open new perp short on new exchange (verified)
|
|
118
|
+
* 4. If the new short definitely failed: re-open the old short (verified)
|
|
119
|
+
* Any leg whose outcome is unknown, or a failed rollback, returns manual (quarantine):
|
|
120
|
+
* retrying could double-short or leave the spot unhedged.
|
|
67
121
|
*/
|
|
68
122
|
rotateShort(params: {
|
|
69
123
|
symbol: string;
|
|
@@ -75,5 +129,5 @@ export declare class SpotPerpExecutor {
|
|
|
75
129
|
size: string;
|
|
76
130
|
leverage: number;
|
|
77
131
|
log: (msg: string) => void;
|
|
78
|
-
}): Promise<
|
|
132
|
+
}): Promise<RotationResult>;
|
|
79
133
|
}
|