perp-cli 0.12.15 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -31
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +74 -84
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +33 -16
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
|
+
import { isQuarantined, quarantineKey, quarantineSymbol } from "../quarantine.js";
|
|
3
|
+
import { PerpError } from "../../errors.js";
|
|
4
|
+
import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToHedge } from "../perp-order.js";
|
|
5
|
+
import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
|
|
6
|
+
import { isPositionGone, assertFundingArbLegs, cancelBotSymbols } from "./funding-arb-utils.js";
|
|
7
|
+
/** Consecutive failed closes before the symbol is quarantined. */
|
|
8
|
+
const MAX_CLOSE_FAILURES = 3;
|
|
2
9
|
export class FundingArbV2Strategy {
|
|
3
10
|
name = "funding-arb-v2";
|
|
4
11
|
_config = {};
|
|
12
|
+
/** Symbols this bot entered or recovered this run — the only symbols its stop cancels orders on. */
|
|
13
|
+
_traded = new Set();
|
|
5
14
|
describe() {
|
|
6
15
|
return {
|
|
7
16
|
description: "Enhanced multi-exchange funding rate arbitrage with auto-close",
|
|
@@ -20,11 +29,68 @@ export class FundingArbV2Strategy {
|
|
|
20
29
|
get sizeUsd() { return Number(this._config.sizeUsd ?? 50); }
|
|
21
30
|
get scanIntervalSec() { return Number(this._config.scanIntervalSec ?? 60); }
|
|
22
31
|
async init(ctx, _snapshot) {
|
|
32
|
+
assertFundingArbLegs(ctx);
|
|
23
33
|
this._config = ctx.config;
|
|
24
|
-
|
|
34
|
+
// A restart must never re-enter (or ignore) what is already held: rebuild the pairs from the venues.
|
|
35
|
+
const recovered = await this.recoverPositions(this.getAdapters(ctx), ctx);
|
|
36
|
+
for (const p of recovered)
|
|
37
|
+
this._traded.add(p.symbol);
|
|
38
|
+
ctx.state.set("positions", recovered);
|
|
25
39
|
ctx.state.set("lastScan", 0);
|
|
40
|
+
if (recovered.length > 0)
|
|
41
|
+
ctx.log(` [ARB-V2] Recovered ${recovered.length} pair(s): ${recovered.map(p => `${p.symbol} ${p.longExchange}↔${p.shortExchange} ${p.size}`).join(", ")}`);
|
|
26
42
|
ctx.log(` [ARB-V2] Ready | minSpread=${this.minSpread}% closeSpread=${this.closeSpread}% maxPos=${this.maxPositions} size=$${this.sizeUsd}`);
|
|
27
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Pairs held on the venues: a long on one venue + a short of the same symbol and EXACTLY the
|
|
46
|
+
* same size on another. A single leg or a size-mismatched long/short is quarantined (both
|
|
47
|
+
* sizes recorded) — never tracked, never stacked on. A venue whose positions cannot be read
|
|
48
|
+
* throws (the start fails and is retried): unknown exposure is never "flat".
|
|
49
|
+
*/
|
|
50
|
+
async recoverPositions(adapters, ctx) {
|
|
51
|
+
const legs = [];
|
|
52
|
+
for (const [venue, a] of adapters) {
|
|
53
|
+
let list;
|
|
54
|
+
try {
|
|
55
|
+
list = await a.getPositions();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
throw new PerpError("EXCHANGE_ERROR", `[ARB-V2] ${venue} positions unreadable at start (${e instanceof Error ? e.message : String(e)}) — not started (held pairs unknown)`, {
|
|
59
|
+
exchange: venue, remediation: `Check ${venue} connectivity (perp -e ${venue} account positions); the bot retries the start`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const p of list) {
|
|
63
|
+
const size = Math.abs(Number(p.size));
|
|
64
|
+
if (size > 0)
|
|
65
|
+
legs.push({ venue, symbol: p.symbol.toUpperCase(), side: p.side, size, raw: p.size.replace(/^-/, "") });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const used = new Set();
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const l of legs) {
|
|
71
|
+
if (l.side !== "long" || used.has(l))
|
|
72
|
+
continue;
|
|
73
|
+
const s = legs.find(x => !used.has(x) && x.side === "short" && x.symbol === l.symbol && x.venue !== l.venue && x.size === l.size);
|
|
74
|
+
if (s === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
used.add(l);
|
|
77
|
+
used.add(s);
|
|
78
|
+
out.push({ symbol: l.symbol, longExchange: l.venue, shortExchange: s.venue, entrySpread: 0, entryTime: 0, sizeUsd: 0, size: l.raw, longClosed: false, closeFailures: 0 });
|
|
79
|
+
}
|
|
80
|
+
const leftover = new Map();
|
|
81
|
+
for (const l of legs)
|
|
82
|
+
if (!used.has(l))
|
|
83
|
+
leftover.set(quarantineKey(l.symbol), [...(leftover.get(quarantineKey(l.symbol)) ?? []), l]);
|
|
84
|
+
for (const [key, group] of leftover) {
|
|
85
|
+
if (isQuarantined(key, ctx.log))
|
|
86
|
+
continue;
|
|
87
|
+
const desc = group.map(l => `${l.side} ${l.raw} on ${l.venue}`).join(", ");
|
|
88
|
+
const venues = [...new Set(group.map(l => l.venue))];
|
|
89
|
+
const mismatched = group.some(l => l.side === "long") && group.some(l => l.side === "short");
|
|
90
|
+
quarantineSymbol(this.name, key, mismatched ? `recovery: ${key} long/short sizes do not match (${desc}) — not tracked as a pair` : `recovery: unpaired ${key} perp leg (${desc}) — no hedge found, not tracked`, `Check ${key} on ${venues.join(" and ")} (${venues.map(v => `perp -e ${v} positions`).join("; ")}); close or re-hedge by hand, then: perp strategy quarantine-clear ${key}`, ctx.log);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
28
94
|
async onTick(ctx, _snapshot) {
|
|
29
95
|
const positions = ctx.state.get("positions");
|
|
30
96
|
const lastScan = ctx.state.get("lastScan");
|
|
@@ -54,14 +120,17 @@ export class FundingArbV2Strategy {
|
|
|
54
120
|
}
|
|
55
121
|
ratesByExchange.set(name, map);
|
|
56
122
|
}
|
|
57
|
-
catch {
|
|
58
|
-
//
|
|
123
|
+
catch (err) {
|
|
124
|
+
// Logged; this venue is left out of this scan (no close decision, no entry on it).
|
|
125
|
+
ctx.log(` [ARB-V2] ${name} markets unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
126
|
}
|
|
60
127
|
}
|
|
61
128
|
// Phase 1: Check existing positions for auto-close
|
|
62
129
|
const toClose = [];
|
|
63
130
|
for (let i = 0; i < positions.length; i++) {
|
|
64
131
|
const pos = positions[i];
|
|
132
|
+
if (isQuarantined(pos.symbol, ctx.log))
|
|
133
|
+
continue;
|
|
65
134
|
const longRates = ratesByExchange.get(pos.longExchange);
|
|
66
135
|
const shortRates = ratesByExchange.get(pos.shortExchange);
|
|
67
136
|
if (!longRates || !shortRates)
|
|
@@ -73,26 +142,8 @@ export class FundingArbV2Strategy {
|
|
|
73
142
|
const currentSpread = this.annualizeSpread(shortInfo.rate, longInfo.rate);
|
|
74
143
|
if (currentSpread <= this.closeSpread) {
|
|
75
144
|
ctx.log(` [ARB-V2] Auto-close ${pos.symbol}: spread ${currentSpread.toFixed(1)}% <= ${this.closeSpread}% (entry was ${pos.entrySpread.toFixed(1)}%)`);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const shortAdapter = adapters.get(pos.shortExchange);
|
|
79
|
-
if (longAdapter && shortAdapter) {
|
|
80
|
-
const price = longInfo.price;
|
|
81
|
-
const size = price > 0 ? (pos.sizeUsd / price).toFixed(6) : "0";
|
|
82
|
-
try {
|
|
83
|
-
await Promise.all([
|
|
84
|
-
longAdapter.marketOrder(pos.symbol, "sell", size),
|
|
85
|
-
shortAdapter.marketOrder(pos.symbol, "buy", size),
|
|
86
|
-
]);
|
|
87
|
-
ctx.log(` [ARB-V2] Closed ${pos.symbol} (long ${pos.longExchange}, short ${pos.shortExchange})`);
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
91
|
-
ctx.log(` [ARB-V2] Close failed for ${pos.symbol}: ${msg}`);
|
|
92
|
-
continue; // Don't mark as closed
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
toClose.push(i);
|
|
145
|
+
if (await this.closePosition(ctx, pos, adapters))
|
|
146
|
+
toClose.push(i);
|
|
96
147
|
}
|
|
97
148
|
}
|
|
98
149
|
// Remove closed positions (reverse order to preserve indices)
|
|
@@ -103,6 +154,11 @@ export class FundingArbV2Strategy {
|
|
|
103
154
|
// Phase 2: Scan for new opportunities
|
|
104
155
|
if (positions.length >= this.maxPositions)
|
|
105
156
|
return [];
|
|
157
|
+
// Engine: equity unknown → no new entries (the closes above still ran).
|
|
158
|
+
if (ctx.state.get("equityUnknown") === true) {
|
|
159
|
+
ctx.log(` [ARB-V2] Equity unknown — no new entries this scan`);
|
|
160
|
+
return positions.length > 0 ? [{ type: "noop" }] : [];
|
|
161
|
+
}
|
|
106
162
|
const exchangeNames = [...ratesByExchange.keys()];
|
|
107
163
|
const allSymbols = new Set();
|
|
108
164
|
for (const [, map] of ratesByExchange) {
|
|
@@ -115,6 +171,8 @@ export class FundingArbV2Strategy {
|
|
|
115
171
|
for (const sym of allSymbols) {
|
|
116
172
|
if (activeSymbols.has(sym))
|
|
117
173
|
continue;
|
|
174
|
+
if (isQuarantined(sym, ctx.log))
|
|
175
|
+
continue;
|
|
118
176
|
let minRate = Infinity, maxRate = -Infinity;
|
|
119
177
|
let minExchange = "", maxExchange = "";
|
|
120
178
|
let bestPrice = 0;
|
|
@@ -149,18 +207,19 @@ export class FundingArbV2Strategy {
|
|
|
149
207
|
// Open new positions
|
|
150
208
|
const slotsAvailable = this.maxPositions - positions.length;
|
|
151
209
|
const toOpen = opportunities.slice(0, slotsAvailable);
|
|
210
|
+
const live = new Map();
|
|
152
211
|
for (const opp of toOpen) {
|
|
153
212
|
const longAdapter = adapters.get(opp.longExchange);
|
|
154
213
|
const shortAdapter = adapters.get(opp.shortExchange);
|
|
155
214
|
if (!longAdapter || !shortAdapter || opp.price <= 0)
|
|
156
215
|
continue;
|
|
216
|
+
if (await this.heldUntracked(opp.symbol, [[opp.longExchange, longAdapter], [opp.shortExchange, shortAdapter]], live, ctx))
|
|
217
|
+
continue;
|
|
218
|
+
this._traded.add(opp.symbol);
|
|
157
219
|
const size = (this.sizeUsd / opp.price).toFixed(6);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
longAdapter.marketOrder(opp.symbol, "buy", size),
|
|
162
|
-
shortAdapter.marketOrder(opp.symbol, "sell", size),
|
|
163
|
-
]);
|
|
220
|
+
ctx.log(` [ARB-V2] Opening ${opp.symbol}: spread ${opp.spread.toFixed(1)}% (long ${opp.longExchange}, short ${opp.shortExchange})`);
|
|
221
|
+
const opened = await this.openPosition(ctx, opp, size, longAdapter, shortAdapter);
|
|
222
|
+
if (opened !== null) {
|
|
164
223
|
positions.push({
|
|
165
224
|
symbol: opp.symbol,
|
|
166
225
|
longExchange: opp.longExchange,
|
|
@@ -168,39 +227,191 @@ export class FundingArbV2Strategy {
|
|
|
168
227
|
entrySpread: opp.spread,
|
|
169
228
|
entryTime: now,
|
|
170
229
|
sizeUsd: this.sizeUsd,
|
|
230
|
+
size: opened,
|
|
231
|
+
longClosed: false,
|
|
232
|
+
closeFailures: 0,
|
|
171
233
|
});
|
|
172
234
|
ctx.log(` [ARB-V2] Position opened! (${positions.length}/${this.maxPositions})`);
|
|
173
235
|
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
-
ctx.log(` [ARB-V2] Execution failed: ${msg}`);
|
|
177
|
-
}
|
|
178
236
|
}
|
|
179
237
|
ctx.state.set("positions", positions);
|
|
180
238
|
// Return noop — arb executes directly via multi-adapter coordination
|
|
181
239
|
return positions.length > 0 || toOpen.length > 0 ? [{ type: "noop" }] : [];
|
|
182
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Close every tracked position (reduce-only, verified). A position that cannot be
|
|
243
|
+
* closed is never dropped silently: it is quarantined (persisted with what is open)
|
|
244
|
+
* so no bot / arb path touches it until a human resolves it.
|
|
245
|
+
*/
|
|
183
246
|
async onStop(ctx) {
|
|
184
247
|
const positions = ctx.state.get("positions");
|
|
185
|
-
if (positions.length === 0)
|
|
186
|
-
return [];
|
|
187
248
|
const adapters = this.getAdapters(ctx);
|
|
249
|
+
const remaining = [];
|
|
188
250
|
for (const pos of positions) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
251
|
+
if (isQuarantined(pos.symbol, ctx.log)) {
|
|
252
|
+
ctx.log(` [ARB-V2] CRITICAL: ${pos.symbol} is quarantined — left open on stop; resolve manually (perp strategy quarantine)`);
|
|
253
|
+
remaining.push(pos);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
ctx.log(` [ARB-V2] Closing ${pos.symbol} on stop (${pos.size})`);
|
|
257
|
+
if (!(await this.closePosition(ctx, pos, adapters))) {
|
|
258
|
+
remaining.push(pos);
|
|
259
|
+
if (!isQuarantined(pos.symbol, ctx.log)) {
|
|
260
|
+
this.quarantine(pos, `still open when the bot stopped (long ${pos.longClosed ? "closed" : `${pos.size} on ${pos.longExchange}`}, short ${pos.size} on ${pos.shortExchange})`, ctx);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
ctx.state.set("positions", remaining);
|
|
265
|
+
// Only the bot's own symbols, on each venue it used — never an account-wide cancel.
|
|
266
|
+
await cancelBotSymbols(adapters, this._traded, "[ARB-V2]", ctx.log);
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
/** A live position of `symbol` on one of the venues that the bot does not track (or an unreadable venue): no entry on top of it. */
|
|
270
|
+
async heldUntracked(symbol, venues, cache, ctx) {
|
|
271
|
+
const key = quarantineKey(symbol);
|
|
272
|
+
for (const [v, a] of venues) {
|
|
273
|
+
if (!cache.has(v))
|
|
274
|
+
cache.set(v, a.getPositions());
|
|
275
|
+
let list;
|
|
276
|
+
try {
|
|
277
|
+
list = await cache.get(v);
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
ctx.log(` [ARB-V2] Skip ${symbol}: ${v} positions unreadable (${e instanceof Error ? e.message : String(e)}) — no entry on unknown exposure`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (list.some(p => quarantineKey(p.symbol) === key && Number(p.size) !== 0)) {
|
|
284
|
+
ctx.log(` [ARB-V2] Skip ${symbol}: a ${symbol} position on ${v} is not one of the bot's pairs — no entry on top of it`);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
quarantine(pos, reason, ctx) {
|
|
291
|
+
quarantineSymbol(this.name, pos.symbol, reason, `Check ${pos.symbol} on ${pos.longExchange} and ${pos.shortExchange} (perp -e ${pos.longExchange} positions; perp -e ${pos.shortExchange} positions); close or re-hedge by hand, then: perp strategy quarantine-clear ${pos.symbol}`, ctx.log);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Long then short, each verified; the short is sized to the long's verified fill and
|
|
295
|
+
* any excess the short did not cover is trimmed off the long (reduce-only, verified).
|
|
296
|
+
* A definite short failure rolls the long back reduce-only. Returns the hedged size
|
|
297
|
+
* (both legs), or null when nothing is open / the symbol was quarantined.
|
|
298
|
+
*/
|
|
299
|
+
async openPosition(ctx, opp, size, longAdapter, shortAdapter) {
|
|
300
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
301
|
+
const dec = sizeDecimalsOf(size);
|
|
302
|
+
let longFilled;
|
|
303
|
+
try {
|
|
304
|
+
longFilled = (await placePerpVerified(longAdapter, opp.longExchange, opp.symbol, "buy", size)).filledSize;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (isOutcomeUnknown(err))
|
|
308
|
+
this.quarantine(opp, `long leg ${size} on ${opp.longExchange} outcome unknown (${errText(err)}) — no short placed`, ctx);
|
|
309
|
+
else
|
|
310
|
+
ctx.log(` [ARB-V2] Execution failed (long leg): ${errText(err)}`);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const longOpen = fillSizeString(longFilled, dec);
|
|
314
|
+
let shortFilled;
|
|
315
|
+
try {
|
|
316
|
+
shortFilled = (await placePerpVerified(shortAdapter, opp.shortExchange, opp.symbol, "sell", longOpen)).filledSize;
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const msg = errText(err);
|
|
320
|
+
if (isOutcomeUnknown(err)) {
|
|
321
|
+
// The short may exist: closing the long could leave a naked short.
|
|
322
|
+
this.quarantine(opp, `short leg ${longOpen} on ${opp.shortExchange} outcome unknown (${msg}); long ${longOpen} open on ${opp.longExchange} — not rolled back`, ctx);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
ctx.log(` [ARB-V2] Execution failed (short leg): ${msg} — rolling back the long (reduce-only)`);
|
|
326
|
+
try {
|
|
327
|
+
const rb = await closePerpVerified(longAdapter, opp.longExchange, opp.symbol, "sell", longOpen, { decimals: dec });
|
|
328
|
+
if (rb.residual > 0) {
|
|
329
|
+
this.quarantine(opp, `short leg failed (${msg}); the long rollback closed ${fillSizeString(rb.closed, dec)} of ${longOpen}, ${fillSizeString(rb.residual, dec)} still open${rb.note !== null ? ` (${rb.note})` : ""} — orphaned long on ${opp.longExchange}`, ctx);
|
|
330
|
+
return null;
|
|
198
331
|
}
|
|
199
|
-
|
|
332
|
+
ctx.log(` [ARB-V2] Rollback OK: closed long ${longOpen} ${opp.symbol} on ${opp.longExchange}`);
|
|
333
|
+
}
|
|
334
|
+
catch (rbErr) {
|
|
335
|
+
this.quarantine(opp, `short leg failed (${msg}) and the long rollback failed or is unconfirmed (${errText(rbErr)}) — orphaned long ${longOpen} on ${opp.longExchange}`, ctx);
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const hedged = fillSizeString(shortFilled, dec);
|
|
340
|
+
try {
|
|
341
|
+
const trimmed = await trimFirstLegToHedge(longAdapter, opp.longExchange, opp.symbol, "buy", longFilled, shortFilled, dec);
|
|
342
|
+
if (trimmed !== "0")
|
|
343
|
+
ctx.log(` [ARB-V2] Short filled ${hedged} of ${longOpen} — trimmed long by ${trimmed} (reduce-only)`);
|
|
344
|
+
}
|
|
345
|
+
catch (trimErr) {
|
|
346
|
+
this.quarantine(opp, `short filled ${hedged} of ${longOpen}; trimming the long's excess on ${opp.longExchange} failed or is unconfirmed (${errText(trimErr)}) — long ${longOpen} vs short ${hedged}`, ctx);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
return hedged;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Close both legs reduce-only with the opened size, verified. Reduce-only makes a
|
|
353
|
+
* retry safe (a leg already flat is rejected, never re-opened), so a definite failure
|
|
354
|
+
* of the LONG close keeps the (still hedged) position for the next tick; MAX_CLOSE_FAILURES
|
|
355
|
+
* in a row, an unknown outcome, or a short close that definitely failed once the long is
|
|
356
|
+
* closed (naked short) quarantines it. Returns true when both legs are flat.
|
|
357
|
+
*/
|
|
358
|
+
async closePosition(ctx, pos, adapters) {
|
|
359
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
360
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
361
|
+
if (!longAdapter || !shortAdapter) {
|
|
362
|
+
ctx.log(` [ARB-V2] Close ${pos.symbol}: adapter missing (${pos.longExchange}/${pos.shortExchange})`);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
366
|
+
const failed = (why) => {
|
|
367
|
+
pos.closeFailures++;
|
|
368
|
+
ctx.log(` [ARB-V2] Close failed for ${pos.symbol}: ${why} (${pos.closeFailures}/${MAX_CLOSE_FAILURES})`);
|
|
369
|
+
if (pos.closeFailures >= MAX_CLOSE_FAILURES)
|
|
370
|
+
this.quarantine(pos, `close failed ${pos.closeFailures} times in a row (last: ${why})`, ctx);
|
|
371
|
+
return false;
|
|
372
|
+
};
|
|
373
|
+
if (!pos.longClosed) {
|
|
374
|
+
try {
|
|
375
|
+
const lc = await closePerpVerified(longAdapter, pos.longExchange, pos.symbol, "sell", pos.size);
|
|
376
|
+
if (lc.residual > 0) {
|
|
377
|
+
this.quarantine(pos, `close of long on ${pos.longExchange} closed ${lc.closed} of ${pos.size}, ${lc.residual} still open${lc.note !== null ? ` (${lc.note})` : ""}; short ${pos.size} on ${pos.shortExchange} not closed`, ctx);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
pos.longClosed = true;
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const msg = errText(err);
|
|
384
|
+
if (isOutcomeUnknown(err)) {
|
|
385
|
+
this.quarantine(pos, `close of long ${pos.size} on ${pos.longExchange} outcome unknown (${msg})`, ctx);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (!isPositionGone(err))
|
|
389
|
+
return failed(`long leg: ${msg}`);
|
|
390
|
+
pos.longClosed = true; // already flat
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const sc = await closePerpVerified(shortAdapter, pos.shortExchange, pos.symbol, "buy", pos.size);
|
|
395
|
+
if (sc.residual > 0) {
|
|
396
|
+
this.quarantine(pos, `the long is closed; close of short on ${pos.shortExchange} closed ${sc.closed} of ${pos.size}, ${sc.residual} still open${sc.note !== null ? ` (${sc.note})` : ""} — residual short unhedged`, ctx);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
const msg = errText(err);
|
|
402
|
+
if (isOutcomeUnknown(err)) {
|
|
403
|
+
this.quarantine(pos, `close of short ${pos.size} on ${pos.shortExchange} outcome unknown (${msg}); the long is closed`, ctx);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (!isPositionGone(err)) {
|
|
407
|
+
// The long is closed, so the short is naked: quarantine NOW — never carry a naked
|
|
408
|
+
// short across ticks waiting for MAX_CLOSE_FAILURES.
|
|
409
|
+
this.quarantine(pos, `long closed on ${pos.longExchange}; close of short ${pos.size} on ${pos.shortExchange} failed (${msg}) — naked short`, ctx);
|
|
410
|
+
return false;
|
|
200
411
|
}
|
|
201
412
|
}
|
|
202
|
-
ctx.
|
|
203
|
-
return
|
|
413
|
+
ctx.log(` [ARB-V2] Closed ${pos.symbol} (long ${pos.longExchange}, short ${pos.shortExchange})`);
|
|
414
|
+
return true;
|
|
204
415
|
}
|
|
205
416
|
getAdapters(ctx) {
|
|
206
417
|
const adapters = new Map();
|