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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
|
-
import { toHourlyRate,
|
|
2
|
+
import { toHourlyRate, annualizeHourlyRate } from "../../funding/normalize.js";
|
|
3
|
+
import { isQuarantined, quarantineSymbol } from "../quarantine.js";
|
|
4
|
+
import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToHedge } from "../perp-order.js";
|
|
5
|
+
import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
|
|
6
|
+
import { PerpError } from "../../errors.js";
|
|
7
|
+
import { assertFundingArbLegs } from "./funding-arb-utils.js";
|
|
8
|
+
/** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis (§10.1). */
|
|
9
|
+
function hourlyOf(info, exchange) {
|
|
10
|
+
return info.fundingHours !== null ? info.rate / info.fundingHours : toHourlyRate(info.rate, exchange);
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Minimum notional per exchange (USD):
|
|
5
14
|
* - HL: ~$2 (0.001 ETH)
|
|
@@ -8,19 +17,14 @@ import { toHourlyRate, computeAnnualSpread, annualizeHourlyRate } from "../../fu
|
|
|
8
17
|
* Use $12 as safe minimum across all exchanges.
|
|
9
18
|
*/
|
|
10
19
|
const MIN_NOTIONAL_USD = 12;
|
|
11
|
-
/**
|
|
20
|
+
/** Entry size precision per asset price range, FLOORED (never rounds up past the notional). */
|
|
12
21
|
function formatSizeForPrice(rawSize, price) {
|
|
13
|
-
// Higher price → fewer decimals (BTC: 4, ETH: 3, SOL: 1,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (price > 1000)
|
|
17
|
-
return rawSize.toFixed(3); // ETH
|
|
18
|
-
if (price > 10)
|
|
19
|
-
return rawSize.toFixed(2); // SOL, AVAX
|
|
20
|
-
if (price > 1)
|
|
21
|
-
return rawSize.toFixed(1); // DOGE, etc
|
|
22
|
-
return rawSize.toFixed(0); // sub-$1 tokens
|
|
22
|
+
// Higher price → fewer decimals (BTC: 4, ETH: 3, SOL: 2, DOGE: 1, sub-$1: 0)
|
|
23
|
+
const decimals = price > 10000 ? 4 : price > 1000 ? 3 : price > 10 ? 2 : price > 1 ? 1 : 0;
|
|
24
|
+
return fillSizeString(rawSize, decimals);
|
|
23
25
|
}
|
|
26
|
+
/** Consecutive definite close failures before a pair is quarantined. */
|
|
27
|
+
const MAX_CLOSE_FAILURES = 3;
|
|
24
28
|
/** Normalize exchange symbol to base asset: "ETH-PERP" → "ETH", "ETHUSDT" → "ETH" */
|
|
25
29
|
function normalizeSymbol(raw) {
|
|
26
30
|
return raw.toUpperCase().replace(/-PERP$/, "").replace(/USDT$/, "").replace(/USD$/, "").replace(/-USD$/, "");
|
|
@@ -41,11 +45,9 @@ class FundingAutoStrategy {
|
|
|
41
45
|
name = "funding-auto";
|
|
42
46
|
_config = {};
|
|
43
47
|
// Config accessors
|
|
44
|
-
get spotPerpRatio() { return Number(this._config.spotPerpRatio ?? 0.7); }
|
|
45
48
|
get perpPerpRatio() { return Number(this._config.perpPerpRatio ?? 0.3); }
|
|
46
49
|
get perpPerpMinSpread() { return Number(this._config.perpPerpMinSpread ?? 20); }
|
|
47
|
-
get
|
|
48
|
-
get perpPerpCloseSpread() { return Number(this._config.perpPerpCloseSpread ?? 5); }
|
|
50
|
+
get perpPerpCloseSpread() { return Number(this._config.perpPerpCloseSpread ?? 0); }
|
|
49
51
|
get maxBreakEvenHours() { return Number(this._config.maxBreakEvenHours ?? 48); }
|
|
50
52
|
get maxPositions() { return Number(this._config.maxPositions ?? 5); }
|
|
51
53
|
get maxCapitalPct() { return Number(this._config.maxCapitalPct ?? 20); }
|
|
@@ -54,8 +56,6 @@ class FundingAutoStrategy {
|
|
|
54
56
|
get scanIntervalTicks() { return Number(this._config.scanIntervalTicks ?? 1); }
|
|
55
57
|
/** Position size: min of sizeUsd (if set) and maxCapitalPct% of total equity */
|
|
56
58
|
_cachedEquity = 0;
|
|
57
|
-
/** Symbols managed by this strategy instance (persists across ticks, prevents adopting unrelated positions) */
|
|
58
|
-
_managedSymbols = new Set();
|
|
59
59
|
getSizeUsd() {
|
|
60
60
|
const fixedSize = Number(this._config.sizeUsd ?? 0);
|
|
61
61
|
const capitalLimit = this._cachedEquity * (this.maxCapitalPct / 100);
|
|
@@ -67,12 +67,11 @@ class FundingAutoStrategy {
|
|
|
67
67
|
}
|
|
68
68
|
describe() {
|
|
69
69
|
return {
|
|
70
|
-
description: "Automated funding rate trading - perp-perp
|
|
70
|
+
description: "Automated funding rate trading - perp-perp (spot-perp mode not supported; use spot-perp-arb)",
|
|
71
71
|
params: [
|
|
72
|
-
{ name: "spotPerpRatio", type: "number", required: false, default: 0
|
|
72
|
+
{ name: "spotPerpRatio", type: "number", required: false, default: 0, description: "Spot-perp mode is NOT supported here (a value > 0 fails with CAPABILITY_UNSUPPORTED) — use spot-perp-arb" },
|
|
73
73
|
{ name: "perpPerpRatio", type: "number", required: false, default: 0.3, description: "Capital ratio for perp-perp mode" },
|
|
74
74
|
{ name: "perpPerpMinSpread", type: "number", required: false, default: 20, description: "Min annualized spread % for perp-perp entry" },
|
|
75
|
-
{ name: "spotPerpMinSpread", type: "number", required: false, default: 5, description: "Min annualized spread % for spot-perp entry" },
|
|
76
75
|
{ name: "perpPerpCloseSpread", type: "number", required: false, default: 0, description: "Close perp-perp when spread reverses below this % (0 = only on reversal)" },
|
|
77
76
|
{ name: "maxBreakEvenHours", type: "number", required: false, default: 48, description: "Max break-even hours to enter" },
|
|
78
77
|
{ name: "maxPositions", type: "number", required: false, default: 5, description: "Max concurrent positions" },
|
|
@@ -85,10 +84,16 @@ class FundingAutoStrategy {
|
|
|
85
84
|
};
|
|
86
85
|
}
|
|
87
86
|
async init(ctx, _snapshot) {
|
|
87
|
+
assertFundingArbLegs(ctx);
|
|
88
88
|
this._config = ctx.config;
|
|
89
|
+
if (ctx.config.spotPerpRatio !== undefined && Number(ctx.config.spotPerpRatio) > 0) {
|
|
90
|
+
throw new PerpError("CAPABILITY_UNSUPPORTED", "funding-auto does not support spot-perp mode (no spot leg can be held or unwound by this strategy)", {
|
|
91
|
+
remediation: "Remove spotPerpRatio (or set it to 0) to run perp-perp only, or use the spot-perp-arb strategy for spot + perp short",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
89
94
|
ctx.state.set("lastScan", 0);
|
|
90
95
|
ctx.state.set("priceCache", new Map());
|
|
91
|
-
ctx.log(`[funding-auto] Starting
|
|
96
|
+
ctx.log(`[funding-auto] Starting (perp-perp only; spot-perp mode is not supported by this strategy)`);
|
|
92
97
|
}
|
|
93
98
|
async onTick(ctx, _snapshot) {
|
|
94
99
|
// Cache total equity across ALL exchanges for capital-based sizing
|
|
@@ -112,7 +117,9 @@ class FundingAutoStrategy {
|
|
|
112
117
|
return [];
|
|
113
118
|
}
|
|
114
119
|
// ── Discover existing arb pairs from live exchange positions ──
|
|
115
|
-
|
|
120
|
+
// A venue whose positions could not be read is never treated as "no positions":
|
|
121
|
+
// no entries touch it this tick (its existing pairs are simply not visible).
|
|
122
|
+
const { pairs: positions, unreadable } = await this.discoverArbPairs(adapters, ctx.log);
|
|
116
123
|
// Fetch funding rates from all exchanges (price data cached for position sizing)
|
|
117
124
|
const ratesByExchange = new Map();
|
|
118
125
|
const priceCache = new Map(); // "exchange:symbol" -> price
|
|
@@ -128,6 +135,7 @@ class FundingAutoStrategy {
|
|
|
128
135
|
rate: m.fundingRate != null ? parseFloat(m.fundingRate) : NaN,
|
|
129
136
|
price,
|
|
130
137
|
exchange: name,
|
|
138
|
+
fundingHours: m.fundingHours,
|
|
131
139
|
});
|
|
132
140
|
if (price > 0) {
|
|
133
141
|
priceCache.set(`${name}:${sym}`, price);
|
|
@@ -153,6 +161,11 @@ class FundingAutoStrategy {
|
|
|
153
161
|
// ── Phase 1: Monitor existing positions for exit ──
|
|
154
162
|
const surviving = [];
|
|
155
163
|
for (const pos of positions) {
|
|
164
|
+
if (isQuarantined(pos.symbol, ctx.log)) {
|
|
165
|
+
// Unknown / unhedged outcome recorded by some bot or arb path: hands off (design §12.3).
|
|
166
|
+
surviving.push(pos);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
156
169
|
if (pos.mode === "perp-perp") {
|
|
157
170
|
// Check if spread has converged below close threshold
|
|
158
171
|
const longRates = ratesByExchange.get(pos.longExchange);
|
|
@@ -167,12 +180,21 @@ class FundingAutoStrategy {
|
|
|
167
180
|
surviving.push(pos);
|
|
168
181
|
continue;
|
|
169
182
|
}
|
|
170
|
-
|
|
171
|
-
//
|
|
183
|
+
// SIGNED spread for the held direction: short-venue hourly − long-venue hourly
|
|
184
|
+
// (each normalized by its own funding basis), annualized %. Positive = still
|
|
185
|
+
// earning; ≤ 0 = the pair has reversed and now pays.
|
|
186
|
+
const currentSpread = annualizeHourlyRate(hourlyOf(shortInfo, pos.shortExchange) - hourlyOf(longInfo, pos.longExchange));
|
|
187
|
+
// Calculate real P&L from exchange history. An unreadable history is never "0":
|
|
188
|
+
// no close decision for this pair this tick.
|
|
172
189
|
const [longPnl, shortPnl] = await Promise.all([
|
|
173
|
-
this.calculateRealPnl(adapters.get(pos.longExchange), pos.symbol),
|
|
174
|
-
this.calculateRealPnl(adapters.get(pos.shortExchange), pos.symbol),
|
|
190
|
+
this.calculateRealPnl(adapters.get(pos.longExchange), pos.longExchange, pos.symbol, ctx.log),
|
|
191
|
+
this.calculateRealPnl(adapters.get(pos.shortExchange), pos.shortExchange, pos.symbol, ctx.log),
|
|
175
192
|
]);
|
|
193
|
+
if (longPnl === null || shortPnl === null) {
|
|
194
|
+
ctx.log(`[funding-auto] ${pos.symbol}: P&L history unreadable — skipping the exit decision this tick`);
|
|
195
|
+
surviving.push(pos);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
176
198
|
const totalFees = longPnl.fees + shortPnl.fees;
|
|
177
199
|
const totalFunding = longPnl.funding + shortPnl.funding;
|
|
178
200
|
pos.accumulatedFunding = totalFunding;
|
|
@@ -180,45 +202,14 @@ class FundingAutoStrategy {
|
|
|
180
202
|
ctx.log(`[funding-auto] ${pos.symbol} spread ${currentSpread.toFixed(1)}% | ` +
|
|
181
203
|
`fees $${totalFees.toFixed(4)} | funding $${totalFunding.toFixed(4)} | ` +
|
|
182
204
|
`net $${(totalFunding - totalFees).toFixed(4)} (${pos.longExchange}↔${pos.shortExchange})`);
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
if (currentSpread <=
|
|
186
|
-
ctx.log(`[funding-auto] Closing perp-perp ${pos.symbol}: spread
|
|
187
|
-
await this.closePosition(ctx, pos, adapters)
|
|
188
|
-
|
|
189
|
-
else {
|
|
190
|
-
surviving.push(pos);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else if (pos.mode === "spot-perp") {
|
|
194
|
-
// Spot-perp: exit if perp funding flips negative (we are short perp)
|
|
195
|
-
const shortRates = ratesByExchange.get(pos.shortExchange);
|
|
196
|
-
if (!shortRates) {
|
|
197
|
-
surviving.push(pos);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const shortInfo = shortRates.get(pos.symbol);
|
|
201
|
-
if (!shortInfo) {
|
|
202
|
-
surviving.push(pos);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const hourlyRate = toHourlyRate(shortInfo.rate, pos.shortExchange);
|
|
206
|
-
const annualized = annualizeHourlyRate(Math.abs(hourlyRate));
|
|
207
|
-
const pnl = await this.calculateRealPnl(adapters.get(pos.shortExchange), pos.symbol);
|
|
208
|
-
pos.accumulatedFunding = pnl.funding;
|
|
209
|
-
pos.totalCost = pnl.fees;
|
|
210
|
-
ctx.log(`[funding-auto] ${pos.symbol} rate ${annualized.toFixed(1)}% | ` +
|
|
211
|
-
`fees $${pnl.fees.toFixed(4)} | funding $${pnl.funding.toFixed(4)} | ` +
|
|
212
|
-
`net $${(pnl.funding - pnl.fees).toFixed(4)} (${pos.shortExchange})`);
|
|
213
|
-
// Exit if funding flipped (rate went negative — shorts would pay instead of receive)
|
|
214
|
-
if (shortInfo.rate < 0) {
|
|
215
|
-
ctx.log(`[funding-auto] Closing spot-perp ${pos.symbol}: funding flipped negative (${(shortInfo.rate * 100).toFixed(4)}%)`);
|
|
216
|
-
await this.closePosition(ctx, pos, adapters);
|
|
205
|
+
// Close when the signed spread falls to the configured threshold (default 0 =
|
|
206
|
+
// only on reversal). A small positive spread still earns funding; closing costs more.
|
|
207
|
+
if (currentSpread <= this.perpPerpCloseSpread) {
|
|
208
|
+
ctx.log(`[funding-auto] Closing perp-perp ${pos.symbol}: spread ${currentSpread.toFixed(1)}% <= close threshold ${this.perpPerpCloseSpread}%${currentSpread <= 0 ? " (REVERSED)" : ""}`);
|
|
209
|
+
if (!(await this.closePosition(ctx, pos, adapters)))
|
|
210
|
+
surviving.push(pos);
|
|
217
211
|
}
|
|
218
212
|
else {
|
|
219
|
-
if (annualized < this.spotPerpMinSpread) {
|
|
220
|
-
ctx.log(`[funding-auto] Spot-perp ${pos.symbol} annualized rate dropped to ${annualized.toFixed(1)}% — monitoring`);
|
|
221
|
-
}
|
|
222
213
|
surviving.push(pos);
|
|
223
214
|
}
|
|
224
215
|
}
|
|
@@ -253,7 +244,7 @@ class FundingAutoStrategy {
|
|
|
253
244
|
const info = ratesByExchange.get(exName)?.get(sym);
|
|
254
245
|
if (!info)
|
|
255
246
|
continue;
|
|
256
|
-
const hourly =
|
|
247
|
+
const hourly = hourlyOf(info, exName);
|
|
257
248
|
if (hourly < minRate) {
|
|
258
249
|
minRate = hourly;
|
|
259
250
|
minExchange = exName;
|
|
@@ -268,7 +259,7 @@ class FundingAutoStrategy {
|
|
|
268
259
|
// Need raw rates for computeAnnualSpread
|
|
269
260
|
const longInfo = ratesByExchange.get(minExchange).get(sym);
|
|
270
261
|
const shortInfo = ratesByExchange.get(maxExchange).get(sym);
|
|
271
|
-
const annualSpread =
|
|
262
|
+
const annualSpread = Math.abs(hourlyOf(shortInfo, maxExchange) - hourlyOf(longInfo, minExchange)) * 24 * 365 * 100;
|
|
272
263
|
if (annualSpread < this.perpPerpMinSpread)
|
|
273
264
|
continue;
|
|
274
265
|
// Calculate cost and break-even
|
|
@@ -293,9 +284,7 @@ class FundingAutoStrategy {
|
|
|
293
284
|
});
|
|
294
285
|
}
|
|
295
286
|
// ── Spot-Perp opportunities ──
|
|
296
|
-
//
|
|
297
|
-
// Opening perp short without spot buy creates naked short = directional risk.
|
|
298
|
-
// TODO: Enable when SpotAdapter is wired into the strategy context.
|
|
287
|
+
// Not supported in this strategy (CAPABILITY_UNSUPPORTED at init) — see the header.
|
|
299
288
|
// Sort by break-even hours ascending (best opportunities first)
|
|
300
289
|
opportunities.sort((a, b) => a.breakEvenHours - b.breakEvenHours);
|
|
301
290
|
if (opportunities.length > 0) {
|
|
@@ -305,22 +294,22 @@ class FundingAutoStrategy {
|
|
|
305
294
|
}
|
|
306
295
|
}
|
|
307
296
|
else {
|
|
308
|
-
ctx.log(`[funding-auto] No opportunities found (perp-perp min ${this.perpPerpMinSpread}%,
|
|
297
|
+
ctx.log(`[funding-auto] No opportunities found (perp-perp min ${this.perpPerpMinSpread}%, BE max ${this.maxBreakEvenHours}h)`);
|
|
309
298
|
}
|
|
310
299
|
// ── Phase 3: Enter new positions ──
|
|
311
300
|
const slotsAvailable = this.maxPositions - surviving.length;
|
|
312
301
|
// Allocate slots by ratio
|
|
313
302
|
const perpPerpSlots = Math.max(1, Math.floor(slotsAvailable * this.perpPerpRatio));
|
|
314
|
-
const spotPerpSlots = Math.max(1, slotsAvailable - perpPerpSlots);
|
|
315
303
|
let perpPerpOpened = 0;
|
|
316
|
-
let spotPerpOpened = 0;
|
|
317
304
|
let currentCount = surviving.length;
|
|
318
305
|
for (const opp of opportunities) {
|
|
319
306
|
if (currentCount >= this.maxPositions)
|
|
320
307
|
break;
|
|
321
|
-
if (
|
|
308
|
+
if (perpPerpOpened >= perpPerpSlots)
|
|
322
309
|
continue;
|
|
323
|
-
if (opp.
|
|
310
|
+
if (unreadable.has(opp.longExchange) || unreadable.has(opp.shortExchange))
|
|
311
|
+
continue;
|
|
312
|
+
if (isQuarantined(opp.symbol, ctx.log))
|
|
324
313
|
continue;
|
|
325
314
|
// Track attempted symbols to prevent re-entry on same tick cycle
|
|
326
315
|
attempted.add(`${opp.symbol}:${opp.mode}`);
|
|
@@ -328,11 +317,7 @@ class FundingAutoStrategy {
|
|
|
328
317
|
const success = await this.openPosition(ctx, opp, adapters);
|
|
329
318
|
if (success) {
|
|
330
319
|
currentCount++;
|
|
331
|
-
|
|
332
|
-
if (opp.mode === "perp-perp")
|
|
333
|
-
perpPerpOpened++;
|
|
334
|
-
else
|
|
335
|
-
spotPerpOpened++;
|
|
320
|
+
perpPerpOpened++;
|
|
336
321
|
ctx.log(`[funding-auto] Opened ${opp.mode} ${opp.symbol}: spread ${opp.annualSpread.toFixed(1)}% | ` +
|
|
337
322
|
`BE ${opp.breakEvenHours.toFixed(1)}h | income $${opp.hourlyIncome.toFixed(4)}/h | ` +
|
|
338
323
|
`long ${opp.longExchange}, short ${opp.shortExchange} (${currentCount}/${this.maxPositions})`);
|
|
@@ -342,13 +327,20 @@ class FundingAutoStrategy {
|
|
|
342
327
|
}
|
|
343
328
|
async onStop(ctx) {
|
|
344
329
|
const adapters = this.getAdapters(ctx);
|
|
345
|
-
const positions = await this.discoverArbPairs(adapters);
|
|
330
|
+
const { pairs: positions, unreadable } = await this.discoverArbPairs(adapters, ctx.log);
|
|
331
|
+
if (unreadable.size > 0) {
|
|
332
|
+
ctx.log(`[funding-auto] CRITICAL: positions on ${[...unreadable].join(", ")} could not be read — any pair there is NOT closed on stop; check manually`);
|
|
333
|
+
}
|
|
346
334
|
if (positions.length === 0) {
|
|
347
335
|
ctx.log("[funding-auto] No positions to close.");
|
|
348
336
|
return [];
|
|
349
337
|
}
|
|
350
338
|
ctx.log(`[funding-auto] Stopping - closing ${positions.length} positions`);
|
|
351
339
|
for (const pos of positions) {
|
|
340
|
+
if (isQuarantined(pos.symbol, ctx.log)) {
|
|
341
|
+
ctx.log(`[funding-auto] CRITICAL: ${pos.symbol} is quarantined — left open on stop; resolve manually (perp strategy quarantine)`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
352
344
|
try {
|
|
353
345
|
await this.closePosition(ctx, pos, adapters);
|
|
354
346
|
}
|
|
@@ -381,9 +373,10 @@ class FundingAutoStrategy {
|
|
|
381
373
|
const price = rawSize > 0 ? this.getSizeUsd() / rawSize : 0;
|
|
382
374
|
return formatSizeForPrice(rawSize, price);
|
|
383
375
|
}
|
|
384
|
-
/** Discover existing arb pairs from live exchange positions */
|
|
385
|
-
async discoverArbPairs(adapters) {
|
|
376
|
+
/** Discover existing arb pairs from live exchange positions; `unreadable` = venues whose positions failed to load. */
|
|
377
|
+
async discoverArbPairs(adapters, log) {
|
|
386
378
|
const allPositions = new Map();
|
|
379
|
+
const unreadable = new Set();
|
|
387
380
|
for (const [name, adapter] of adapters) {
|
|
388
381
|
try {
|
|
389
382
|
const exchangePositions = await adapter.getPositions();
|
|
@@ -395,15 +388,19 @@ class FundingAutoStrategy {
|
|
|
395
388
|
exchange: name,
|
|
396
389
|
side: p.side,
|
|
397
390
|
size: Math.abs(Number(p.size)),
|
|
391
|
+
sizeRaw: String(p.size).trim().replace(/^[-+]/, ""), // venue's own size string, for closes
|
|
398
392
|
symbol: p.symbol, // raw for order execution
|
|
399
393
|
});
|
|
400
394
|
}
|
|
401
395
|
}
|
|
402
|
-
catch {
|
|
396
|
+
catch (e) {
|
|
397
|
+
unreadable.add(name);
|
|
398
|
+
log(`[funding-auto] ${name} positions unreadable (${e instanceof Error ? e.message : String(e)}) — no entries on ${name} this tick`);
|
|
399
|
+
}
|
|
403
400
|
}
|
|
404
|
-
// Find arb pairs: same symbol, opposite sides, different exchanges = perp-perp
|
|
401
|
+
// Find arb pairs: same symbol, opposite sides, different exchanges = perp-perp.
|
|
402
|
+
// An unmatched short is never adopted (spot-perp mode is not supported here).
|
|
405
403
|
const pairs = [];
|
|
406
|
-
const matchedShorts = new Set(); // track shorts already matched to perp-perp
|
|
407
404
|
for (const [sym, positionList] of allPositions) {
|
|
408
405
|
const longs = positionList.filter(p => p.side === "long");
|
|
409
406
|
const shorts = positionList.filter(p => p.side === "short");
|
|
@@ -420,43 +417,22 @@ class FundingAutoStrategy {
|
|
|
420
417
|
shortSize: short.size,
|
|
421
418
|
longSymbol: long.symbol,
|
|
422
419
|
shortSymbol: short.symbol,
|
|
420
|
+
longSizeRaw: long.sizeRaw,
|
|
421
|
+
shortSizeRaw: short.sizeRaw,
|
|
423
422
|
entrySpread: 0,
|
|
424
423
|
entryTime: 0,
|
|
425
424
|
notional: 0,
|
|
426
425
|
totalCost: 0,
|
|
427
426
|
accumulatedFunding: 0,
|
|
428
427
|
});
|
|
429
|
-
matchedShorts.add(`${sym}:${short.exchange}`);
|
|
430
428
|
}
|
|
431
429
|
}
|
|
432
430
|
}
|
|
433
|
-
// Unmatched shorts = spot-perp ONLY if funding-auto previously entered them
|
|
434
|
-
// (prevents adopting unrelated positions like manual PURR short)
|
|
435
|
-
for (const short of shorts) {
|
|
436
|
-
if (!matchedShorts.has(`${sym}:${short.exchange}`) && this._managedSymbols.has(`${sym}:spot-perp`)) {
|
|
437
|
-
pairs.push({
|
|
438
|
-
id: `${sym}-spot-${short.exchange}`,
|
|
439
|
-
symbol: sym,
|
|
440
|
-
mode: "spot-perp",
|
|
441
|
-
longExchange: short.exchange,
|
|
442
|
-
shortExchange: short.exchange,
|
|
443
|
-
longSize: 0,
|
|
444
|
-
shortSize: short.size,
|
|
445
|
-
longSymbol: "",
|
|
446
|
-
shortSymbol: short.symbol,
|
|
447
|
-
entrySpread: 0,
|
|
448
|
-
entryTime: 0,
|
|
449
|
-
notional: 0,
|
|
450
|
-
totalCost: 0,
|
|
451
|
-
accumulatedFunding: 0,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
431
|
}
|
|
456
|
-
return pairs;
|
|
432
|
+
return { pairs, unreadable };
|
|
457
433
|
}
|
|
458
|
-
/**
|
|
459
|
-
async calculateRealPnl(adapter, symbol) {
|
|
434
|
+
/** Actual fees / funding from exchange trade/funding history; null when the history could not be read (never 0). */
|
|
435
|
+
async calculateRealPnl(adapter, exchange, symbol, log) {
|
|
460
436
|
try {
|
|
461
437
|
const [trades, funding] = await Promise.all([
|
|
462
438
|
adapter.getTradeHistory(50),
|
|
@@ -468,105 +444,150 @@ class FundingAutoStrategy {
|
|
|
468
444
|
const totalFunding = symFunding.reduce((s, f) => s + Number(f.payment), 0);
|
|
469
445
|
return { fees: totalFees, funding: totalFunding };
|
|
470
446
|
}
|
|
471
|
-
catch {
|
|
472
|
-
|
|
447
|
+
catch (e) {
|
|
448
|
+
log(`[funding-auto] ${exchange} ${symbol} trade/funding history unreadable: ${e instanceof Error ? e.message : String(e)}`);
|
|
449
|
+
return null;
|
|
473
450
|
}
|
|
474
451
|
}
|
|
452
|
+
quarantine(ctx, pos, reason) {
|
|
453
|
+
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);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Perp-perp entry, sequential and verified: long first, then the short sized to the
|
|
457
|
+
* long's VERIFIED fill; any excess the short did not cover is trimmed off the long
|
|
458
|
+
* (reduce-only, verified). A definite short failure rolls the long back reduce-only.
|
|
459
|
+
* An unknown outcome or a failed rollback / trim quarantines the symbol.
|
|
460
|
+
*/
|
|
475
461
|
async openPosition(ctx, opp, adapters) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return false;
|
|
481
|
-
const price = this.getCachedPrice(ctx, opp.longExchange, opp.symbol)
|
|
482
|
-
|| this.getCachedPrice(ctx, opp.shortExchange, opp.symbol);
|
|
483
|
-
if (price <= 0)
|
|
484
|
-
return false;
|
|
485
|
-
const notionalUsd = this.getSizeUsd();
|
|
486
|
-
const size = this.formatSize(notionalUsd / price, opp.symbol);
|
|
487
|
-
// Minimum notional check — skip if below exchange minimums
|
|
488
|
-
if (notionalUsd < MIN_NOTIONAL_USD) {
|
|
489
|
-
ctx.log(`[funding-auto] Skip ${opp.symbol}: notional $${notionalUsd.toFixed(0)} < min $${MIN_NOTIONAL_USD}`);
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
try {
|
|
493
|
-
await Promise.all([
|
|
494
|
-
longAdapter.marketOrder(opp.symbol, "buy", size),
|
|
495
|
-
shortAdapter.marketOrder(opp.symbol, "sell", size),
|
|
496
|
-
]);
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
501
|
-
ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol}: ${msg}`);
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
462
|
+
// Engine: equity unknown → no new entries (closes still run).
|
|
463
|
+
if (ctx.state.get("equityUnknown") === true) {
|
|
464
|
+
ctx.log(`[funding-auto] Skip ${opp.symbol}: equity unknown — no new entries`);
|
|
465
|
+
return false;
|
|
504
466
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
467
|
+
const longAdapter = adapters.get(opp.longExchange);
|
|
468
|
+
const shortAdapter = adapters.get(opp.shortExchange);
|
|
469
|
+
if (!longAdapter || !shortAdapter)
|
|
470
|
+
return false;
|
|
471
|
+
const price = this.getCachedPrice(ctx, opp.longExchange, opp.symbol)
|
|
472
|
+
|| this.getCachedPrice(ctx, opp.shortExchange, opp.symbol);
|
|
473
|
+
if (price <= 0)
|
|
474
|
+
return false;
|
|
475
|
+
const notionalUsd = this.getSizeUsd();
|
|
476
|
+
const size = this.formatSize(notionalUsd / price, opp.symbol);
|
|
477
|
+
// Minimum notional check — skip if below exchange minimums
|
|
478
|
+
if (notionalUsd < MIN_NOTIONAL_USD) {
|
|
479
|
+
ctx.log(`[funding-auto] Skip ${opp.symbol}: notional $${notionalUsd.toFixed(0)} < min $${MIN_NOTIONAL_USD}`);
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (!(Number(size) > 0)) {
|
|
483
|
+
ctx.log(`[funding-auto] Skip ${opp.symbol}: size rounds to 0 at $${notionalUsd.toFixed(0)} / $${price}`);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
487
|
+
const dec = sizeDecimalsOf(size);
|
|
488
|
+
let longFilled;
|
|
489
|
+
try {
|
|
490
|
+
longFilled = (await placePerpVerified(longAdapter, opp.longExchange, opp.symbol, "buy", size)).filledSize;
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
if (isOutcomeUnknown(err))
|
|
494
|
+
this.quarantine(ctx, opp, `entry long ${size} on ${opp.longExchange} outcome unknown (${errText(err)}) — no short placed`);
|
|
495
|
+
else
|
|
496
|
+
ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol} (long leg): ${errText(err)}`);
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
const longOpen = fillSizeString(longFilled, dec);
|
|
500
|
+
let shortFilled;
|
|
501
|
+
try {
|
|
502
|
+
shortFilled = (await placePerpVerified(shortAdapter, opp.shortExchange, opp.symbol, "sell", longOpen)).filledSize;
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
const msg = errText(err);
|
|
506
|
+
if (isOutcomeUnknown(err)) {
|
|
507
|
+
// The short may exist: closing the long could leave a naked short.
|
|
508
|
+
this.quarantine(ctx, opp, `entry short ${longOpen} on ${opp.shortExchange} outcome unknown (${msg}); long ${longOpen} open on ${opp.longExchange} — not rolled back`);
|
|
517
509
|
return false;
|
|
518
510
|
}
|
|
511
|
+
ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol} (short leg): ${msg} — rolling back the long (reduce-only)`);
|
|
519
512
|
try {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
513
|
+
const rb = await closePerpVerified(longAdapter, opp.longExchange, opp.symbol, "sell", longOpen, { decimals: dec });
|
|
514
|
+
if (rb.residual > 0) {
|
|
515
|
+
this.quarantine(ctx, opp, `entry short 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}`);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
ctx.log(`[funding-auto] Rollback OK: closed long ${longOpen} ${opp.symbol} on ${opp.longExchange}`);
|
|
524
519
|
}
|
|
525
|
-
catch (
|
|
526
|
-
|
|
527
|
-
ctx.log(`[funding-auto] Spot-perp entry failed ${opp.symbol}: ${msg}`);
|
|
528
|
-
return false;
|
|
520
|
+
catch (rbErr) {
|
|
521
|
+
this.quarantine(ctx, opp, `entry short failed (${msg}) and the long rollback failed or is unconfirmed (${errText(rbErr)}) — orphaned long ${longOpen} on ${opp.longExchange}`);
|
|
529
522
|
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const trimmed = await trimFirstLegToHedge(longAdapter, opp.longExchange, opp.symbol, "buy", longFilled, shortFilled, dec);
|
|
527
|
+
if (trimmed !== "0")
|
|
528
|
+
ctx.log(`[funding-auto] Short filled ${fillSizeString(shortFilled, dec)} of ${longOpen} — trimmed long by ${trimmed} (reduce-only)`);
|
|
529
|
+
}
|
|
530
|
+
catch (trimErr) {
|
|
531
|
+
this.quarantine(ctx, opp, `entry short filled ${fillSizeString(shortFilled, dec)} of ${longOpen}; trimming the long's excess on ${opp.longExchange} failed or is unconfirmed (${errText(trimErr)})`);
|
|
532
|
+
return false;
|
|
530
533
|
}
|
|
534
|
+
return true;
|
|
531
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Close both legs sequentially, reduce-only and verified, each sized from the venue's
|
|
538
|
+
* own position size (never rounded up). Long first: a definite failure there changes
|
|
539
|
+
* nothing, so the pair is retried next tick and quarantined after MAX_CLOSE_FAILURES
|
|
540
|
+
* in a row. Once the long is closed, ANY short failure leaves an unhedged short this
|
|
541
|
+
* strategy can no longer discover as a pair → quarantine. An unknown outcome or a
|
|
542
|
+
* residual that survives one reduce-only residual close quarantines. Returns true when both legs closed.
|
|
543
|
+
*/
|
|
532
544
|
async closePosition(ctx, pos, adapters) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const longSize = formatSizeForPrice(pos.longSize, pos.longSize > 0 && pos.notional > 0 ? pos.notional / pos.longSize : 0);
|
|
539
|
-
const shortSize = formatSizeForPrice(pos.shortSize, pos.shortSize > 0 && pos.notional > 0 ? pos.notional / pos.shortSize : 0);
|
|
540
|
-
try {
|
|
541
|
-
await Promise.all([
|
|
542
|
-
longAdapter.marketOrder(pos.longSymbol, "sell", longSize),
|
|
543
|
-
shortAdapter.marketOrder(pos.shortSymbol, "buy", shortSize),
|
|
544
|
-
]);
|
|
545
|
-
const netPnl = pos.accumulatedFunding - pos.totalCost;
|
|
546
|
-
ctx.log(`[funding-auto] Closed perp-perp ${pos.symbol} | funding $${pos.accumulatedFunding.toFixed(4)} - fees $${pos.totalCost.toFixed(4)} = net $${netPnl.toFixed(4)}`);
|
|
547
|
-
}
|
|
548
|
-
catch (err) {
|
|
549
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
550
|
-
ctx.log(`[funding-auto] Close failed ${pos.symbol}: ${msg}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
545
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
546
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
547
|
+
if (!longAdapter || !shortAdapter) {
|
|
548
|
+
ctx.log(`[funding-auto] Close ${pos.symbol}: adapter missing (${pos.longExchange}/${pos.shortExchange})`);
|
|
549
|
+
return false;
|
|
553
550
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
567
|
-
ctx.log(`[funding-auto] Close failed ${pos.symbol}: ${msg}`);
|
|
551
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
552
|
+
const failures = ctx.state.get("closeFailures") ?? new Map();
|
|
553
|
+
ctx.state.set("closeFailures", failures);
|
|
554
|
+
let lc;
|
|
555
|
+
try {
|
|
556
|
+
lc = await closePerpVerified(longAdapter, pos.longExchange, pos.longSymbol, "sell", pos.longSizeRaw);
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
const msg = errText(err);
|
|
560
|
+
if (isOutcomeUnknown(err)) {
|
|
561
|
+
this.quarantine(ctx, pos, `close of long ${pos.longSizeRaw} on ${pos.longExchange} outcome unknown (${msg}) — short ${pos.shortSizeRaw} on ${pos.shortExchange} left open`);
|
|
562
|
+
return false;
|
|
568
563
|
}
|
|
564
|
+
const n = (failures.get(pos.id) ?? 0) + 1;
|
|
565
|
+
failures.set(pos.id, n);
|
|
566
|
+
ctx.log(`[funding-auto] Close failed ${pos.symbol} (long leg, nothing changed): ${msg} (${n}/${MAX_CLOSE_FAILURES})`);
|
|
567
|
+
if (n >= MAX_CLOSE_FAILURES)
|
|
568
|
+
this.quarantine(ctx, pos, `close failed ${n} times in a row (last: ${msg})`);
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
failures.delete(pos.id);
|
|
572
|
+
if (lc.residual > 0) {
|
|
573
|
+
this.quarantine(ctx, pos, `close of long on ${pos.longExchange} closed ${lc.closed} of ${pos.longSizeRaw}, ${lc.residual} still open${lc.note !== null ? ` (${lc.note})` : ""} — short ${pos.shortSizeRaw} on ${pos.shortExchange} not closed (legs unbalanced)`);
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
let sc;
|
|
577
|
+
try {
|
|
578
|
+
sc = await closePerpVerified(shortAdapter, pos.shortExchange, pos.shortSymbol, "buy", pos.shortSizeRaw);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
this.quarantine(ctx, pos, `long closed on ${pos.longExchange} but the close of short ${pos.shortSizeRaw} on ${pos.shortExchange} ${isOutcomeUnknown(err) ? "has an unknown outcome" : "failed"} (${errText(err)}) — short unhedged`);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (sc.residual > 0) {
|
|
585
|
+
this.quarantine(ctx, pos, `long closed; close of short on ${pos.shortExchange} closed ${sc.closed} of ${pos.shortSizeRaw}, ${sc.residual} still open${sc.note !== null ? ` (${sc.note})` : ""} — residual short unhedged`);
|
|
586
|
+
return false;
|
|
569
587
|
}
|
|
588
|
+
const netPnl = pos.accumulatedFunding - pos.totalCost;
|
|
589
|
+
ctx.log(`[funding-auto] Closed perp-perp ${pos.symbol} | funding $${pos.accumulatedFunding.toFixed(4)} - fees $${pos.totalCost.toFixed(4)} = net $${netPnl.toFixed(4)}`);
|
|
590
|
+
return true;
|
|
570
591
|
}
|
|
571
592
|
}
|
|
572
593
|
registerStrategy("funding-auto", (_config) => new FundingAutoStrategy());
|
|
@@ -29,16 +29,30 @@ export declare class SpotPerpArbStrategy implements Strategy {
|
|
|
29
29
|
};
|
|
30
30
|
private _config;
|
|
31
31
|
private _failCooldown;
|
|
32
|
+
/** Consecutive failed close attempts per symbol (quarantined at MAX_CLOSE_FAILURES). */
|
|
33
|
+
private _closeFailures;
|
|
34
|
+
/** Venues whose positions could not be read during recovery: no entries there until re-read. */
|
|
35
|
+
private _unrecovered;
|
|
36
|
+
/** Perp symbols this bot entered or tracked this run — the only symbols its stop cancels orders on. */
|
|
37
|
+
private _traded;
|
|
32
38
|
private _fundingScoreCache;
|
|
33
39
|
private get params();
|
|
34
40
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
35
41
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
36
42
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
43
|
+
/** Close recorded in the arb state: entry removed (remaining null) or its legs set to the still-hedged remainder. */
|
|
44
|
+
private recordClose;
|
|
45
|
+
private quarantined;
|
|
46
|
+
/** Unknown / unhedged outcome (executor `manual`): stop touching this symbol until a human clears it. */
|
|
47
|
+
private quarantine;
|
|
37
48
|
private scoreFundingLocal;
|
|
49
|
+
/** base (upper-case) → perp venues it may hedge on (canonical symbol + price guard, `botSpotPerpMatches`). */
|
|
50
|
+
private spotPerpVenues;
|
|
38
51
|
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
|
|
39
52
|
private findPerpCandidates;
|
|
40
53
|
private calcAnnualized;
|
|
41
54
|
private shouldClose;
|
|
42
55
|
private tryOpenPosition;
|
|
56
|
+
/** Rebuild positions from the venues; `unreadable` = venues whose positions / spot balances failed to load (never "none"). */
|
|
43
57
|
private recoverPositions;
|
|
44
58
|
}
|