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
package/dist/plan-executor.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { assertCanClosePositions } from "./exchanges/trading-v2.js";
|
|
1
2
|
import { symbolMatch } from "./utils.js";
|
|
2
3
|
import { logExecution } from "./execution-log.js";
|
|
3
4
|
import { randomUUID } from "crypto";
|
|
5
|
+
import { invalidateCache } from "./cache.js";
|
|
6
|
+
import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderOutcomeUnknown } from "./errors.js";
|
|
7
|
+
import { closePerpVerified, isOutcomeUnknown, placePerpVerified } from "./bot/perp-order.js";
|
|
8
|
+
import { fillSizeString, sizeDecimalsOf } from "./exchanges/spot-result.js";
|
|
9
|
+
import { quarantineSymbol } from "./bot/quarantine.js";
|
|
10
|
+
import { resolveAdapterSlippage } from "./exchanges/slippage.js";
|
|
4
11
|
/**
|
|
5
12
|
* Validate a plan structure without executing it.
|
|
6
13
|
*/
|
|
@@ -67,6 +74,8 @@ export function validatePlan(plan) {
|
|
|
67
74
|
/**
|
|
68
75
|
* Execute a plan step by step.
|
|
69
76
|
*/
|
|
77
|
+
/** Plan actions that place orders (open or close positions). */
|
|
78
|
+
const PLAN_ORDER_ACTIONS = new Set(["market_order", "limit_order", "stop_order", "close_position"]);
|
|
70
79
|
export async function executePlan(adapter, plan, opts) {
|
|
71
80
|
const planId = randomUUID().slice(0, 8);
|
|
72
81
|
const startTime = Date.now();
|
|
@@ -74,18 +83,40 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
74
83
|
const completedSteps = new Map();
|
|
75
84
|
const dryRun = opts?.dryRun ?? false;
|
|
76
85
|
const log = opts?.log ?? (() => { });
|
|
86
|
+
// A plan that places orders must be able to close them (rollback / close_position are
|
|
87
|
+
// reduce-only): refuse before the first step on a venue that cannot.
|
|
88
|
+
if (!dryRun && plan.steps.some(s => PLAN_ORDER_ACTIONS.has(s.action))) {
|
|
89
|
+
assertCanClosePositions(adapter, `Plan on ${adapter.name}`);
|
|
90
|
+
}
|
|
91
|
+
/** Order steps that completed, with their measured fill (market orders) for rollback. */
|
|
92
|
+
const placed = new Map();
|
|
93
|
+
const quarantined = new Set();
|
|
94
|
+
const finish = (status) => ({
|
|
95
|
+
planId,
|
|
96
|
+
status: quarantined.size > 0 && status !== "pending" ? "quarantined" : status,
|
|
97
|
+
steps: results,
|
|
98
|
+
quarantined: [...quarantined],
|
|
99
|
+
totalDurationMs: Date.now() - startTime,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
});
|
|
102
|
+
const quarantine = (symbol, reason) => {
|
|
103
|
+
quarantineSymbol("plan-execute", symbol, `plan ${planId}: ${reason}`, `Check the ${symbol} position and open orders on ${adapter.name} (perp -e ${adapter.name} account positions / trade orders); fix by hand, then: perp strategy quarantine-clear ${symbol}`, log);
|
|
104
|
+
quarantined.add(symbol.toUpperCase());
|
|
105
|
+
};
|
|
77
106
|
log(`[plan:${planId}] Starting ${plan.steps.length} steps (${dryRun ? "DRY RUN" : "LIVE"})`);
|
|
78
107
|
for (const step of plan.steps) {
|
|
79
108
|
const stepStart = Date.now();
|
|
80
109
|
// Check dependency
|
|
81
110
|
if (step.dependsOn) {
|
|
82
111
|
const dep = completedSteps.get(step.dependsOn);
|
|
83
|
-
|
|
112
|
+
// Only a dependency that actually succeeded (or was dry-run) lets the step run:
|
|
113
|
+
// failed / skipped / pending / rolled-back / missing all block it.
|
|
114
|
+
if (!dep || (dep.status !== "success" && dep.status !== "dry_run")) {
|
|
84
115
|
const result = {
|
|
85
116
|
stepId: step.id,
|
|
86
117
|
action: step.action,
|
|
87
118
|
status: "skipped",
|
|
88
|
-
error: { code: "DEPENDENCY_FAILED", message: `Depends on ${step.dependsOn} which
|
|
119
|
+
error: { code: "DEPENDENCY_FAILED", message: `Depends on ${step.dependsOn} which is ${dep ? dep.status : "missing"}` },
|
|
89
120
|
durationMs: 0,
|
|
90
121
|
};
|
|
91
122
|
results.push(result);
|
|
@@ -107,7 +138,7 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
107
138
|
continue;
|
|
108
139
|
}
|
|
109
140
|
try {
|
|
110
|
-
const result = await executeStep(adapter, step, log);
|
|
141
|
+
const result = await executeStep(adapter, step, log, placed);
|
|
111
142
|
const stepResult = {
|
|
112
143
|
stepId: step.id,
|
|
113
144
|
action: step.action,
|
|
@@ -120,8 +151,28 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
120
151
|
log(`[plan:${planId}] Step ${step.id} (${step.action}): success (${stepResult.durationMs}ms)`);
|
|
121
152
|
}
|
|
122
153
|
catch (err) {
|
|
123
|
-
const message =
|
|
154
|
+
const message = extractErrorMessage(err);
|
|
124
155
|
const onFailure = step.onFailure ?? "abort";
|
|
156
|
+
if (ORDER_ACTIONS.has(step.action) && isOutcomeUnknown(err)) {
|
|
157
|
+
// The order may have been placed: never treat it as failed, never resubmit or roll it
|
|
158
|
+
// back. Stop the plan and quarantine the symbol (design §12.3).
|
|
159
|
+
const sym = String(step.params.symbol ?? "").toUpperCase();
|
|
160
|
+
log(`[plan:${planId}] Step ${step.id} (${step.action}): OUTCOME UNKNOWN — ${message}`);
|
|
161
|
+
quarantine(sym, `step ${step.id} (${step.action}) outcome unknown: ${message}`);
|
|
162
|
+
results.push({
|
|
163
|
+
stepId: step.id,
|
|
164
|
+
action: step.action,
|
|
165
|
+
status: "pending",
|
|
166
|
+
error: { code: "ORDER_PENDING", message },
|
|
167
|
+
durationMs: Date.now() - stepStart,
|
|
168
|
+
});
|
|
169
|
+
if (onFailure === "rollback") {
|
|
170
|
+
// Reverse completed steps on OTHER symbols only — the pending order's symbol is
|
|
171
|
+
// quarantined and left untouched (a reversal there could not be measured).
|
|
172
|
+
await rollbackSteps(adapter, results.filter(r => r.status === "success"), placed, sym, quarantine, log);
|
|
173
|
+
}
|
|
174
|
+
return finish("pending");
|
|
175
|
+
}
|
|
125
176
|
log(`[plan:${planId}] Step ${step.id} (${step.action}): FAILED — ${message}`);
|
|
126
177
|
if (onFailure === "skip") {
|
|
127
178
|
results.push({
|
|
@@ -136,7 +187,7 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
136
187
|
}
|
|
137
188
|
if (onFailure === "rollback") {
|
|
138
189
|
log(`[plan:${planId}] Rolling back completed steps...`);
|
|
139
|
-
await rollbackSteps(adapter, results.filter(r => r.status === "success"), log);
|
|
190
|
+
await rollbackSteps(adapter, results.filter(r => r.status === "success"), placed, null, quarantine, log);
|
|
140
191
|
results.push({
|
|
141
192
|
stepId: step.id,
|
|
142
193
|
action: step.action,
|
|
@@ -144,13 +195,7 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
144
195
|
error: { code: "STEP_FAILED", message },
|
|
145
196
|
durationMs: Date.now() - stepStart,
|
|
146
197
|
});
|
|
147
|
-
return
|
|
148
|
-
planId,
|
|
149
|
-
status: "failed",
|
|
150
|
-
steps: results,
|
|
151
|
-
totalDurationMs: Date.now() - startTime,
|
|
152
|
-
timestamp: new Date().toISOString(),
|
|
153
|
-
};
|
|
198
|
+
return finish("failed");
|
|
154
199
|
}
|
|
155
200
|
// abort
|
|
156
201
|
results.push({
|
|
@@ -160,44 +205,80 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
160
205
|
error: { code: "STEP_FAILED", message },
|
|
161
206
|
durationMs: Date.now() - stepStart,
|
|
162
207
|
});
|
|
163
|
-
return
|
|
164
|
-
planId,
|
|
165
|
-
status: results.some(r => r.status === "success") ? "partial" : "failed",
|
|
166
|
-
steps: results,
|
|
167
|
-
totalDurationMs: Date.now() - startTime,
|
|
168
|
-
timestamp: new Date().toISOString(),
|
|
169
|
-
};
|
|
208
|
+
return finish(results.some(r => r.status === "success") ? "partial" : "failed");
|
|
170
209
|
}
|
|
171
210
|
}
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
211
|
+
return finish(dryRun ? "dry_run" : "completed");
|
|
212
|
+
}
|
|
213
|
+
/** Steps that send an order (a thrown non-definite error = outcome unknown). */
|
|
214
|
+
const ORDER_ACTIONS = new Set(["market_order", "limit_order", "stop_order", "close_position"]);
|
|
215
|
+
/**
|
|
216
|
+
* Order id from a venue placement response: TradingV2 OrderResult.orderId, Aster/Binance
|
|
217
|
+
* `orderId`, Pacifica `order_id`, HL `response.data.statuses[0].resting.oid`, generic `id`.
|
|
218
|
+
* null when none is present (the rollback then refuses to guess).
|
|
219
|
+
*/
|
|
220
|
+
function extractOrderId(result) {
|
|
221
|
+
if (result === null || typeof result !== "object")
|
|
222
|
+
return null;
|
|
223
|
+
const r = result;
|
|
224
|
+
for (const k of ["orderId", "order_id", "oid", "id"]) {
|
|
225
|
+
const v = r[k];
|
|
226
|
+
if (typeof v === "string" && v.length > 0)
|
|
227
|
+
return v;
|
|
228
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
229
|
+
return String(v);
|
|
230
|
+
}
|
|
231
|
+
const data = (r.data ?? r.response?.data);
|
|
232
|
+
const statuses = data?.statuses;
|
|
233
|
+
for (const k of ["resting", "filled"]) { // HL: resting order, or a limit that filled immediately
|
|
234
|
+
const st = statuses?.[0]?.[k];
|
|
235
|
+
if (st && (typeof st.oid === "number" || typeof st.oid === "string"))
|
|
236
|
+
return String(st.oid);
|
|
237
|
+
}
|
|
238
|
+
if (data)
|
|
239
|
+
return extractOrderId(data);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
/** Limit / stop placement: a typed definite failure stays; anything else may have placed the order → ORDER_PENDING. */
|
|
243
|
+
async function sendRestingOrder(adapter, what, send) {
|
|
244
|
+
try {
|
|
245
|
+
return await send();
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
throw isDefiniteOrderFailure(e) ? e : orderOutcomeUnknown(adapter.name, what, e);
|
|
249
|
+
}
|
|
179
250
|
}
|
|
180
|
-
async function executeStep(adapter, step, log) {
|
|
251
|
+
async function executeStep(adapter, step, log, placed) {
|
|
181
252
|
const p = step.params;
|
|
182
253
|
const symbol = String(p.symbol ?? "").toUpperCase();
|
|
183
254
|
const side = String(p.side ?? "");
|
|
184
255
|
const size = String(p.size ?? "");
|
|
185
256
|
switch (step.action) {
|
|
186
257
|
case "market_order": {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
258
|
+
// Fill MEASURED as the position delta (ORDER_PENDING when unconfirmed) — the rollback
|
|
259
|
+
// reverses exactly this size.
|
|
260
|
+
const fill = await placePerpVerified(adapter, adapter.name, symbol, side, size);
|
|
261
|
+
placed.set(step.id, { symbol, side, filledSize: fill.filledSize, decimals: sizeDecimalsOf(size), orderId: null });
|
|
262
|
+
logExecution({ type: "market_order", exchange: adapter.name, symbol, side, size, status: "success", dryRun: false, meta: { planStep: step.id, clientId: step.clientId, filledSize: fill.filledSize } });
|
|
263
|
+
return fill.raw;
|
|
190
264
|
}
|
|
191
265
|
case "limit_order": {
|
|
192
266
|
const price = String(p.price ?? "");
|
|
193
|
-
const result = await adapter.limitOrder(symbol, side, price, size);
|
|
267
|
+
const result = await sendRestingOrder(adapter, `limit ${side} ${size} ${symbol} @ ${price}`, () => adapter.limitOrder(symbol, side, price, size));
|
|
268
|
+
placed.set(step.id, { symbol, side, filledSize: null, decimals: sizeDecimalsOf(size), orderId: extractOrderId(result) });
|
|
194
269
|
logExecution({ type: "limit_order", exchange: adapter.name, symbol, side, size, price, status: "success", dryRun: false, meta: { planStep: step.id, clientId: step.clientId } });
|
|
195
270
|
return result;
|
|
196
271
|
}
|
|
197
272
|
case "stop_order": {
|
|
198
273
|
const triggerPrice = String(p.triggerPrice ?? "");
|
|
199
274
|
const limitPrice = p.limitPrice ? String(p.limitPrice) : undefined;
|
|
200
|
-
|
|
275
|
+
const result = await sendRestingOrder(adapter, `stop ${side} ${size} ${symbol} @ ${triggerPrice}`, () => adapter.stopOrder(symbol, side, size, triggerPrice, {
|
|
276
|
+
limitPrice, reduceOnly: !!p.reduceOnly,
|
|
277
|
+
// Market triggers carry the resolved bound (protection-priced on Lighter / Arcus; native triggers ignore it).
|
|
278
|
+
...(limitPrice === undefined ? { slippage: resolveAdapterSlippage(adapter, null) } : {}),
|
|
279
|
+
}));
|
|
280
|
+
placed.set(step.id, { symbol, side, filledSize: null, decimals: sizeDecimalsOf(size), orderId: extractOrderId(result) });
|
|
281
|
+
return result;
|
|
201
282
|
}
|
|
202
283
|
case "cancel_order": {
|
|
203
284
|
return adapter.cancelOrder(symbol, String(p.orderId ?? ""));
|
|
@@ -211,14 +292,23 @@ async function executeStep(adapter, step, log) {
|
|
|
211
292
|
return adapter.setLeverage(symbol, lev, mode);
|
|
212
293
|
}
|
|
213
294
|
case "close_position": {
|
|
295
|
+
invalidateCache("acct"); // size the reduce-only close from a fresh read, never a cached position
|
|
214
296
|
const positions = await adapter.getPositions();
|
|
215
297
|
const pos = positions.find(pp => symbolMatch(pp.symbol, symbol));
|
|
216
298
|
if (!pos)
|
|
217
299
|
throw new Error(`No position found for ${symbol}`);
|
|
218
300
|
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
301
|
+
// Size from the live position (abs), reduce-only: a stale / repeated close can never open
|
|
302
|
+
// or flip a position — the venue rejects it instead. Fill measured (ORDER_PENDING when unconfirmed).
|
|
303
|
+
// A partial close (venue-final IOC partial fills are accepted, §13.10 #4) gets ONE residual
|
|
304
|
+
// close (closePerpVerified); a residual still open fails the step with ORDER_PARTIAL.
|
|
305
|
+
const liveSize = pos.size.replace(/^-/, "");
|
|
306
|
+
const c = await closePerpVerified(adapter, adapter.name, pos.symbol, closeSide, liveSize);
|
|
307
|
+
logExecution({ type: "market_order", exchange: adapter.name, symbol, side: closeSide, size: liveSize, status: c.residual > 0 ? "failed" : "success", dryRun: false, meta: { planStep: step.id, action: "close_position", reduceOnly: true, filledSize: c.closed, residual: c.residual } });
|
|
308
|
+
if (c.residual > 0) {
|
|
309
|
+
throw new PerpError("ORDER_PARTIAL", `close_position ${pos.symbol}: closed ${c.closed} of ${liveSize}, ${c.residual} still open${c.note !== null ? ` (${c.note})` : ""}`, { exchange: adapter.name });
|
|
310
|
+
}
|
|
311
|
+
return { symbol: pos.symbol, side: closeSide, size: liveSize, closed: c.closed };
|
|
222
312
|
}
|
|
223
313
|
case "wait": {
|
|
224
314
|
const ms = Number(p.ms ?? 1000);
|
|
@@ -248,33 +338,54 @@ async function executeStep(adapter, step, log) {
|
|
|
248
338
|
}
|
|
249
339
|
}
|
|
250
340
|
/**
|
|
251
|
-
*
|
|
341
|
+
* Reverse completed steps (reverse order). A filled market order is closed reduce-only for
|
|
342
|
+
* its MEASURED fill (closePerpVerified, residual handled); a limit / stop order's resting
|
|
343
|
+
* orders are cancelled on that step's symbol only. A reversal that is rejected, unconfirmed
|
|
344
|
+
* or partial marks the step rollback_failed and quarantines the symbol. Steps on
|
|
345
|
+
* `skipSymbol` (an order whose outcome is unknown) are left untouched.
|
|
252
346
|
*/
|
|
253
|
-
async function rollbackSteps(adapter, completedSteps, log) {
|
|
254
|
-
// Rollback in reverse order
|
|
347
|
+
async function rollbackSteps(adapter, completedSteps, placed, skipSymbol, quarantine, log) {
|
|
255
348
|
for (let i = completedSteps.length - 1; i >= 0; i--) {
|
|
256
349
|
const step = completedSteps[i];
|
|
350
|
+
const order = placed.get(step.stepId);
|
|
351
|
+
if (order === undefined)
|
|
352
|
+
continue; // not an order step (cancel / leverage / checks / close_position): nothing to reverse
|
|
353
|
+
if (skipSymbol !== null && order.symbol === skipSymbol) {
|
|
354
|
+
log(`[rollback] Leaving ${step.stepId} (${order.symbol}) — the symbol has an order with unknown outcome (quarantined)`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
257
357
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
358
|
+
if (order.filledSize !== null) {
|
|
359
|
+
const size = fillSizeString(order.filledSize, order.decimals);
|
|
360
|
+
if (!(Number(size) > 0))
|
|
361
|
+
continue;
|
|
362
|
+
const closeSide = order.side === "buy" ? "sell" : "buy";
|
|
363
|
+
log(`[rollback] Reversing ${step.stepId}: reduce-only ${closeSide} ${size} ${order.symbol}`);
|
|
364
|
+
const c = await closePerpVerified(adapter, adapter.name, order.symbol, closeSide, size, { decimals: order.decimals });
|
|
365
|
+
if (c.residual > 0)
|
|
366
|
+
throw new Error(`closed ${c.closed}; ${c.residual} still open${c.note !== null ? ` (${c.note})` : ""}`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Resting limit / stop: cancel ONLY this step's order (never the user's other orders
|
|
370
|
+
// on the symbol). Any fill before the cancel is not reversed — the step is marked
|
|
371
|
+
// "cancelled" (not "rolled_back") and the log says to check the position.
|
|
372
|
+
if (order.orderId === null) {
|
|
373
|
+
throw new Error(`no order id was returned for ${step.stepId}; cancel it manually (perp trade cancel ${order.symbol} <orderId>)`);
|
|
267
374
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
break;
|
|
375
|
+
log(`[rollback] Cancelling ${step.stepId} order ${order.orderId} on ${order.symbol}`);
|
|
376
|
+
await adapter.cancelOrder(order.symbol, order.orderId);
|
|
377
|
+
log(`[rollback] ${step.stepId}: any partial fill before the cancel is NOT reversed — check ${order.symbol} position`);
|
|
378
|
+
step.status = "cancelled";
|
|
379
|
+
continue;
|
|
274
380
|
}
|
|
381
|
+
step.status = "rolled_back";
|
|
275
382
|
}
|
|
276
383
|
catch (err) {
|
|
277
|
-
|
|
384
|
+
const message = extractErrorMessage(err);
|
|
385
|
+
log(`[rollback] Failed to rollback step ${step.stepId}: ${message}`);
|
|
386
|
+
step.status = "rollback_failed";
|
|
387
|
+
step.error = { code: isOutcomeUnknown(err) ? "ORDER_PENDING" : "ROLLBACK_FAILED", message };
|
|
388
|
+
quarantine(order.symbol, `rollback of step ${step.stepId} (${step.action} ${order.side} ${order.symbol}) ${isOutcomeUnknown(err) ? "unconfirmed" : "failed"}: ${message}`);
|
|
278
389
|
}
|
|
279
390
|
}
|
|
280
391
|
}
|
package/dist/position-history.js
CHANGED
|
@@ -110,6 +110,15 @@ export function getPositionStats(opts) {
|
|
|
110
110
|
for (const r of records) {
|
|
111
111
|
stats.totalTrades++;
|
|
112
112
|
const pnl = Number(r.realizedPnl ?? 0);
|
|
113
|
+
// Rule #2 (qa/2026-05-16 extension): a non-finite realizedPnl from a
|
|
114
|
+
// corrupt history row would propagate NaN into totalPnl / averages and
|
|
115
|
+
// break the entire stats report. Surface the bad row and skip its PnL
|
|
116
|
+
// contribution (the trade is still counted but excluded from PnL math
|
|
117
|
+
// so the operator sees the row count vs. expected and can investigate).
|
|
118
|
+
if (!Number.isFinite(pnl)) {
|
|
119
|
+
process.stderr.write(`[position-history] skipping non-finite realizedPnl for ${r.symbol ?? "<unknown>"} on ${r.exchange ?? "<unknown>"}: ${JSON.stringify(r.realizedPnl)}\n`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
113
122
|
if (pnl > 0)
|
|
114
123
|
stats.wins++;
|
|
115
124
|
else if (pnl < 0)
|
|
@@ -150,6 +159,10 @@ export function getPositionStats(opts) {
|
|
|
150
159
|
const symbolWins = {};
|
|
151
160
|
for (const r of records) {
|
|
152
161
|
const pnl = Number(r.realizedPnl ?? 0);
|
|
162
|
+
// Same guard as above — silently skipping NaN here keeps win-rate
|
|
163
|
+
// attribution consistent with the totals computed in the main loop.
|
|
164
|
+
if (!Number.isFinite(pnl))
|
|
165
|
+
continue;
|
|
153
166
|
if (pnl > 0) {
|
|
154
167
|
symbolWins[r.symbol] = (symbolWins[r.symbol] ?? 0) + 1;
|
|
155
168
|
}
|
package/dist/rebalance.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ExchangeAdapter } from "./exchanges/index.js";
|
|
2
|
+
import "./exchanges/venues/index.js";
|
|
3
|
+
/** Throws NO_BRIDGE_ROUTE when the venue's funds live where no bridge route exists. */
|
|
4
|
+
export declare function assertBridgeRoute(exchange: string): void;
|
|
2
5
|
/**
|
|
3
6
|
* Cross-exchange rebalancing engine.
|
|
4
7
|
*
|
|
@@ -45,9 +48,30 @@ export declare function computeRebalancePlan(snapshots: ExchangeBalanceSnapshot[
|
|
|
45
48
|
reserve?: number;
|
|
46
49
|
}): RebalancePlan;
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
51
|
+
* Headroom over the initial margin an arb leg needs before it is entered: covers taker fees,
|
|
52
|
+
* the slippage bound on the market entry, venue margin rounding and a mark move before the
|
|
53
|
+
* second leg fills (the leg must not be at maintenance margin right after entry).
|
|
49
54
|
*/
|
|
50
|
-
export declare
|
|
55
|
+
export declare const ARB_MARGIN_BUFFER = 1.5;
|
|
56
|
+
/**
|
|
57
|
+
* Margin a leg of `notionalUsd` needs at `leverage` (the leverage the venue applies to the market
|
|
58
|
+
* — the arb does not change it), × ARB_MARGIN_BUFFER. Leverage unknown (null: the venue exposes no
|
|
59
|
+
* per-market setting) → the 1x requirement (the full notional): the most margin any leverage can
|
|
60
|
+
* need, so the check never under-states it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function requiredArbMargin(notionalUsd: number, leverage: number | null, buffer?: number): number;
|
|
63
|
+
/**
|
|
64
|
+
* Check if an exchange has enough available balance to open a leg of `notionalUsd` at
|
|
65
|
+
* `leverage` (requiredArbMargin). No balance snapshot for the exchange → false.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasEnoughBalance(snapshots: ExchangeBalanceSnapshot[], exchange: string, notionalUsd: number, leverage: number | null, marginBuffer?: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The leverage `adapter` applies to `symbol`: adapter.marketLeverage(symbol) where the venue has a
|
|
70
|
+
* per-symbol read (Hyperliquid activeAssetData, Pacifica /account/settings), else the
|
|
71
|
+
* adapter.getMarketSettings() row (Aster, Lighter, Arcus). null when neither lists the symbol;
|
|
72
|
+
* a failed read throws.
|
|
73
|
+
*/
|
|
74
|
+
export declare function marketLeverageOf(adapter: object, symbol: string): Promise<number | null>;
|
|
51
75
|
/**
|
|
52
76
|
* Describe the bridge route for a rebalance move.
|
|
53
77
|
*/
|
package/dist/rebalance.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { PerpError } from "./errors.js";
|
|
2
|
+
import { getVenue, tryResolveVenueId } from "./exchanges/venue.js";
|
|
3
|
+
import "./exchanges/venues/index.js";
|
|
4
|
+
/** Settlement chains with no withdraw → bridge → deposit route in this CLI (design §10.10). */
|
|
5
|
+
const NO_ROUTE_CHAINS = new Set(["robinhood", "offchain"]);
|
|
6
|
+
/** Throws NO_BRIDGE_ROUTE when the venue's funds live where no bridge route exists. */
|
|
7
|
+
export function assertBridgeRoute(exchange) {
|
|
8
|
+
const id = tryResolveVenueId(exchange);
|
|
9
|
+
if (id === null)
|
|
10
|
+
return; // not a venue name — the existing "unknown route" handling applies
|
|
11
|
+
const def = getVenue(id);
|
|
12
|
+
const chain = def.networks.mainnet.settlementChain;
|
|
13
|
+
if (NO_ROUTE_CHAINS.has(chain)) {
|
|
14
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `${def.displayName} settles on "${chain}"; cross-venue rebalancing has no route there`, {
|
|
15
|
+
remediation: `Exclude ${def.id} from --exchanges and move funds for it manually`,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
1
19
|
import { EXCHANGE_TO_CHAIN } from "./bridge-engine.js";
|
|
2
20
|
// Re-export for backward compat
|
|
3
21
|
const EXCHANGE_CHAINS = Object.fromEntries(Object.entries(EXCHANGE_TO_CHAIN).map(([ex, chain]) => [ex, { chain }]));
|
|
@@ -8,12 +26,35 @@ export async function fetchAllBalances(adapters) {
|
|
|
8
26
|
const entries = [...adapters.entries()];
|
|
9
27
|
const results = await Promise.allSettled(entries.map(async ([name, adapter]) => {
|
|
10
28
|
const bal = await adapter.getBalance();
|
|
29
|
+
// Strict policy (qa/2026-05-16): empty string is corruption, not
|
|
30
|
+
// "venue stringified zero". A truly absent field must surface as
|
|
31
|
+
// undefined/null at the adapter layer, not "". An empty string here
|
|
32
|
+
// would coerce to 0 via Number("") and silently propagate a $0
|
|
33
|
+
// balance into the plan — exactly the Rule #2 silent-substitution
|
|
34
|
+
// hole this audit cycle is closing.
|
|
35
|
+
if (bal.equity === "" || bal.available === "" || bal.marginUsed === "" || bal.unrealizedPnl === "") {
|
|
36
|
+
throw new PerpError("EXCHANGE_ERROR", `${name} returned empty-string balance field: equity="${bal.equity}" available="${bal.available}" marginUsed="${bal.marginUsed}" unrealizedPnl="${bal.unrealizedPnl}"`, { exchange: name });
|
|
37
|
+
}
|
|
38
|
+
const equity = Number(bal.equity);
|
|
39
|
+
const available = Number(bal.available);
|
|
40
|
+
const marginUsed = Number(bal.marginUsed);
|
|
41
|
+
const unrealizedPnl = Number(bal.unrealizedPnl);
|
|
42
|
+
// Rule #2: rebalance plan computes sums and per-exchange targets.
|
|
43
|
+
// A NaN value would silently propagate into the plan output and the
|
|
44
|
+
// execute step would attempt nonsense moves. Reject upfront so the
|
|
45
|
+
// affected exchange falls out of `Promise.allSettled` (filtered to
|
|
46
|
+
// fulfilled below) and the user sees a partial result instead of a
|
|
47
|
+
// corrupt plan.
|
|
48
|
+
if (!Number.isFinite(equity) || !Number.isFinite(available) ||
|
|
49
|
+
!Number.isFinite(marginUsed) || !Number.isFinite(unrealizedPnl)) {
|
|
50
|
+
throw new PerpError("EXCHANGE_ERROR", `${name} returned non-finite balance: equity=${bal.equity} available=${bal.available} marginUsed=${bal.marginUsed} unrealizedPnl=${bal.unrealizedPnl}`, { exchange: name });
|
|
51
|
+
}
|
|
11
52
|
return {
|
|
12
53
|
exchange: name,
|
|
13
|
-
equity
|
|
14
|
-
available
|
|
15
|
-
marginUsed
|
|
16
|
-
unrealizedPnl
|
|
54
|
+
equity,
|
|
55
|
+
available,
|
|
56
|
+
marginUsed,
|
|
57
|
+
unrealizedPnl,
|
|
17
58
|
};
|
|
18
59
|
}));
|
|
19
60
|
return results
|
|
@@ -62,6 +103,8 @@ export function computeRebalancePlan(snapshots, opts = {}) {
|
|
|
62
103
|
const moveAmt = Math.min(remaining, Math.abs(deficitAmt));
|
|
63
104
|
if (moveAmt < minMove)
|
|
64
105
|
continue;
|
|
106
|
+
assertBridgeRoute(fromEx);
|
|
107
|
+
assertBridgeRoute(toEx);
|
|
65
108
|
moves.push({
|
|
66
109
|
from: fromEx,
|
|
67
110
|
to: toEx,
|
|
@@ -84,18 +127,54 @@ export function computeRebalancePlan(snapshots, opts = {}) {
|
|
|
84
127
|
};
|
|
85
128
|
}
|
|
86
129
|
/**
|
|
87
|
-
*
|
|
130
|
+
* Headroom over the initial margin an arb leg needs before it is entered: covers taker fees,
|
|
131
|
+
* the slippage bound on the market entry, venue margin rounding and a mark move before the
|
|
132
|
+
* second leg fills (the leg must not be at maintenance margin right after entry).
|
|
133
|
+
*/
|
|
134
|
+
export const ARB_MARGIN_BUFFER = 1.5;
|
|
135
|
+
/**
|
|
136
|
+
* Margin a leg of `notionalUsd` needs at `leverage` (the leverage the venue applies to the market
|
|
137
|
+
* — the arb does not change it), × ARB_MARGIN_BUFFER. Leverage unknown (null: the venue exposes no
|
|
138
|
+
* per-market setting) → the 1x requirement (the full notional): the most margin any leverage can
|
|
139
|
+
* need, so the check never under-states it.
|
|
140
|
+
*/
|
|
141
|
+
export function requiredArbMargin(notionalUsd, leverage, buffer = ARB_MARGIN_BUFFER) {
|
|
142
|
+
const lev = leverage !== null && Number.isFinite(leverage) && leverage >= 1 ? leverage : 1;
|
|
143
|
+
return (notionalUsd / lev) * buffer;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if an exchange has enough available balance to open a leg of `notionalUsd` at
|
|
147
|
+
* `leverage` (requiredArbMargin). No balance snapshot for the exchange → false.
|
|
88
148
|
*/
|
|
89
|
-
export function hasEnoughBalance(snapshots, exchange,
|
|
149
|
+
export function hasEnoughBalance(snapshots, exchange, notionalUsd, leverage, marginBuffer = ARB_MARGIN_BUFFER) {
|
|
90
150
|
const snap = snapshots.find((s) => s.exchange === exchange);
|
|
91
151
|
if (!snap)
|
|
92
152
|
return false;
|
|
93
|
-
return snap.available >=
|
|
153
|
+
return snap.available >= requiredArbMargin(notionalUsd, leverage, marginBuffer);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The leverage `adapter` applies to `symbol`: adapter.marketLeverage(symbol) where the venue has a
|
|
157
|
+
* per-symbol read (Hyperliquid activeAssetData, Pacifica /account/settings), else the
|
|
158
|
+
* adapter.getMarketSettings() row (Aster, Lighter, Arcus). null when neither lists the symbol;
|
|
159
|
+
* a failed read throws.
|
|
160
|
+
*/
|
|
161
|
+
export async function marketLeverageOf(adapter, symbol) {
|
|
162
|
+
const one = adapter.marketLeverage;
|
|
163
|
+
if (typeof one === "function")
|
|
164
|
+
return one.call(adapter, symbol);
|
|
165
|
+
const read = adapter.getMarketSettings;
|
|
166
|
+
if (typeof read !== "function")
|
|
167
|
+
return null;
|
|
168
|
+
const norm = (x) => x.toUpperCase().replace(/-PERP$/, "").replace(/^[^:]+:/, "");
|
|
169
|
+
const row = (await read.call(adapter)).find(r => norm(r.symbol) === norm(symbol));
|
|
170
|
+
return row !== undefined && row.leverage >= 1 ? row.leverage : null;
|
|
94
171
|
}
|
|
95
172
|
/**
|
|
96
173
|
* Describe the bridge route for a rebalance move.
|
|
97
174
|
*/
|
|
98
175
|
export function describeBridgeRoute(move) {
|
|
176
|
+
assertBridgeRoute(move.from);
|
|
177
|
+
assertBridgeRoute(move.to);
|
|
99
178
|
const from = EXCHANGE_CHAINS[move.from];
|
|
100
179
|
const to = EXCHANGE_CHAINS[move.to];
|
|
101
180
|
if (!from || !to)
|