perp-cli 0.12.17 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
2
|
+
import { MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
1
3
|
import { symbolMatch, logSettledRejections } from "../utils.js";
|
|
4
|
+
import { logExecution } from "../execution-log.js";
|
|
2
5
|
import { updateJobState } from "../jobs.js";
|
|
3
|
-
import {
|
|
6
|
+
import { invalidateCache } from "../cache.js";
|
|
7
|
+
import { fetchAllBalances, computeRebalancePlan, hasEnoughBalance, marketLeverageOf, requiredArbMargin } from "../rebalance.js";
|
|
4
8
|
import { checkArbLiquidity } from "../liquidity.js";
|
|
5
9
|
import { computeAnnualSpread } from "../funding.js";
|
|
6
10
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../shared-api.js";
|
|
7
|
-
import { computeMatchedSize
|
|
11
|
+
import { computeMatchedSize } from "../arb-sizing.js";
|
|
12
|
+
import { closePerpPairVerified, openPerpPairVerified } from "../commands/arb-auto.js";
|
|
13
|
+
import { closePerpVerified, isOutcomeUnknown } from "../bot/perp-order.js";
|
|
14
|
+
import { isQuarantined, quarantineSymbol } from "../bot/quarantine.js";
|
|
15
|
+
import { isPositionGone, matchSymbol } from "../bot/strategies/funding-arb-utils.js";
|
|
16
|
+
/** Quarantine source for `perp strategy funding-arb` (src/bot/quarantine.ts). */
|
|
17
|
+
const SOURCE = "funding-arb-cli";
|
|
8
18
|
async function fetchAllRates() {
|
|
9
19
|
// SSOT rule #2: each DEX is independent. allSettled keeps the comparison
|
|
10
20
|
// alive when one DEX errors out, while rejection reasons get logged to
|
|
@@ -56,7 +66,19 @@ async function fetchAllRates() {
|
|
|
56
66
|
];
|
|
57
67
|
}
|
|
58
68
|
// annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
|
|
69
|
+
/**
|
|
70
|
+
* `perp strategy funding-arb` loop. With autoExecute (design §12.3):
|
|
71
|
+
* - entry = openPerpPairVerified (long, then the short sized to the long's VERIFIED fill;
|
|
72
|
+
* definite short failure → reduce-only rollback; unknown outcome → quarantine). A lot-size
|
|
73
|
+
* rejection is never resubmitted blindly: the long is re-sized only when it was definitely
|
|
74
|
+
* not placed; a rejected short is placed once at the lot-floored verified long fill;
|
|
75
|
+
* - close / max-drawdown close = closePerpPairVerified with the venue position size strings
|
|
76
|
+
* (reduce-only, verified, one residual close, quarantine on unknown / unhedged);
|
|
77
|
+
* - quarantined symbols (src/bot/quarantine.ts) are never entered or closed.
|
|
78
|
+
*/
|
|
59
79
|
export async function runFundingArb(adapters, params, jobId, log = console.log) {
|
|
80
|
+
for (const a of adapters.values())
|
|
81
|
+
assertCanClosePositions(a, `Funding arb on ${a.name}`);
|
|
60
82
|
const maxPos = params.maxPositions ?? 3;
|
|
61
83
|
const closeSpread = params.closeSpread ?? 5;
|
|
62
84
|
const positions = [];
|
|
@@ -105,28 +127,19 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
105
127
|
if (currentSpread <= closeSpread) {
|
|
106
128
|
log(`[ARB] CLOSE signal: ${pos.symbol} spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`);
|
|
107
129
|
if (params.autoExecute) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
await Promise.all([
|
|
113
|
-
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
114
|
-
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
115
|
-
]);
|
|
116
|
-
log(`[ARB] CLOSED ${pos.symbol} — both legs unwound`);
|
|
117
|
-
// Estimate P&L from funding collected
|
|
118
|
-
const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
|
|
119
|
-
// entrySpread is annualized %; convert to per-hour rate
|
|
120
|
-
const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
|
|
121
|
-
const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
|
|
122
|
-
log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
126
|
-
log(`[ARB] Close error: ${msg}`);
|
|
127
|
-
continue; // don't remove position if close failed
|
|
128
|
-
}
|
|
130
|
+
if (isQuarantined(pos.symbol, log)) {
|
|
131
|
+
log(`[ARB] ${pos.symbol} is quarantined — not closing (perp strategy quarantine)`);
|
|
132
|
+
continue;
|
|
129
133
|
}
|
|
134
|
+
const closed = await closeArbPosition(pos, adapters, log);
|
|
135
|
+
if (!closed)
|
|
136
|
+
continue; // still open (failed) or quarantined — keep tracking
|
|
137
|
+
// Estimate P&L from funding collected
|
|
138
|
+
const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
|
|
139
|
+
// entrySpread is annualized %; convert to per-hour rate
|
|
140
|
+
const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
|
|
141
|
+
const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
|
|
142
|
+
log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
|
|
130
143
|
}
|
|
131
144
|
positions.splice(i, 1);
|
|
132
145
|
}
|
|
@@ -139,11 +152,14 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
139
152
|
if (totalPnl < -params.maxDrawdown) {
|
|
140
153
|
log(`[ARB] MAX DRAWDOWN hit: uPnL $${totalPnl.toFixed(2)} < -$${params.maxDrawdown}`);
|
|
141
154
|
log(`[ARB] Closing all ${positions.length} positions...`);
|
|
142
|
-
await closeAllPositions(positions, adapters, log);
|
|
155
|
+
const stillOpen = await closeAllPositions(positions, adapters, log);
|
|
143
156
|
positions.length = 0;
|
|
157
|
+
positions.push(...stillOpen);
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
|
-
catch {
|
|
160
|
+
catch (err) {
|
|
161
|
+
log(`[ARB] Drawdown check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
147
163
|
}
|
|
148
164
|
// ── Phase 3: Balance check + rebalance trigger ──
|
|
149
165
|
let balanceSnapshots = null;
|
|
@@ -178,6 +194,8 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
178
194
|
continue;
|
|
179
195
|
if (positions.length >= maxPos)
|
|
180
196
|
break;
|
|
197
|
+
if (params.autoExecute && isQuarantined(symbol, log))
|
|
198
|
+
continue;
|
|
181
199
|
// Only consider exchanges we have adapters for
|
|
182
200
|
const available = symbolRates.filter((r) => adapters.has(r.exchange));
|
|
183
201
|
if (available.length < 2)
|
|
@@ -187,6 +205,13 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
187
205
|
const highest = available[available.length - 1];
|
|
188
206
|
if (lowest.exchange === highest.exchange)
|
|
189
207
|
continue;
|
|
208
|
+
// Same ticker on two venues is not proof of the same asset: the chosen pair must agree on price.
|
|
209
|
+
const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
|
|
210
|
+
if (!(pairGapPct <= MAX_PRICE_DEVIATION_PCT)) {
|
|
211
|
+
if (cycleCount === 1)
|
|
212
|
+
log(`[ARB] Skip ${symbol} ${lowest.exchange}↔${highest.exchange}: mark gap ${pairGapPct.toFixed(1)}% > ${MAX_PRICE_DEVIATION_PCT}% (likely a different asset)`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
190
215
|
const annualSpread = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
|
|
191
216
|
if (annualSpread >= params.minSpread) {
|
|
192
217
|
const longEx = lowest.exchange;
|
|
@@ -212,64 +237,69 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
212
237
|
}
|
|
213
238
|
// Check balances before trading
|
|
214
239
|
if (balanceSnapshots) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
try {
|
|
222
|
-
log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
|
|
240
|
+
// Margin at the leverage each venue applies to the market (unknown → 1x, see requiredArbMargin).
|
|
241
|
+
let longLev;
|
|
242
|
+
let shortLev;
|
|
223
243
|
try {
|
|
224
|
-
await Promise.all([
|
|
225
|
-
longAdapter.marketOrder(symbol, "buy", matched.size),
|
|
226
|
-
shortAdapter.marketOrder(symbol, "sell", matched.size),
|
|
227
|
-
]);
|
|
244
|
+
[longLev, shortLev] = await Promise.all([marketLeverageOf(longAdapter, symbol), marketLeverageOf(shortAdapter, symbol)]);
|
|
228
245
|
}
|
|
229
|
-
catch (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (lotMatch) {
|
|
233
|
-
const lotSize = parseFloat(lotMatch[1]);
|
|
234
|
-
log(`[ARB] Lot size ${lotSize} detected, recalculating...`);
|
|
235
|
-
matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx, { lotSize });
|
|
236
|
-
if (!matched) {
|
|
237
|
-
log(`[ARB] Skip ${symbol}: can't meet lot size ${lotSize}`);
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
log(`[ARB] Retry: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}/leg)`);
|
|
241
|
-
await Promise.all([
|
|
242
|
-
longAdapter.marketOrder(symbol, "buy", matched.size),
|
|
243
|
-
shortAdapter.marketOrder(symbol, "sell", matched.size),
|
|
244
|
-
]);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
throw orderErr;
|
|
248
|
-
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
log(`[ARB] Skip ${symbol}: could not read the market leverage (${e instanceof Error ? e.message : String(e)})`);
|
|
248
|
+
continue;
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
250
|
+
if (!hasEnoughBalance(balanceSnapshots, longEx, matched.notional, longLev) ||
|
|
251
|
+
!hasEnoughBalance(balanceSnapshots, shortEx, matched.notional, shortLev)) {
|
|
252
|
+
const need = (ex, lev) => `${ex} ~$${requiredArbMargin(matched.notional, lev).toFixed(2)} (${lev !== null ? `${lev}x` : "leverage unknown → 1x"})`;
|
|
253
|
+
log(`[ARB] Skip ${symbol}: insufficient available balance for $${matched.notional.toFixed(2)}/leg — need ${need(longEx, longLev)}, ${need(shortEx, shortLev)}`);
|
|
254
|
+
continue;
|
|
256
255
|
}
|
|
257
|
-
catch { /* non-critical */ }
|
|
258
|
-
positions.push({
|
|
259
|
-
symbol,
|
|
260
|
-
longExchange: longEx,
|
|
261
|
-
shortExchange: shortEx,
|
|
262
|
-
size: matched.size,
|
|
263
|
-
entrySpread: annualSpread,
|
|
264
|
-
openedAt: Date.now(),
|
|
265
|
-
entryPrices: { long: lowest.markPrice, short: highest.markPrice },
|
|
266
|
-
});
|
|
267
|
-
log(`[ARB] OPENED ${symbol} delta-neutral (${positions.length}/${maxPos})`);
|
|
268
256
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
257
|
+
log(`[ARB] Opening: ${matched.size} ${symbol} — long ${longEx} then short ${shortEx}, each verified ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
|
|
258
|
+
const openPair = (size) => openPerpPairVerified({
|
|
259
|
+
source: SOURCE, sym: symbol,
|
|
260
|
+
longAdapter, longExch: longEx, longSym: symbol,
|
|
261
|
+
shortAdapter, shortExch: shortEx, shortSym: symbol,
|
|
262
|
+
size, smart: false, log, shortLotRetry: true,
|
|
263
|
+
});
|
|
264
|
+
let res = await openPair(matched.size);
|
|
265
|
+
if (res.status === "not_opened" && res.rolledBack === null && res.lotSize !== null) {
|
|
266
|
+
// The LONG was definitely rejected for lot size (nothing placed): recompute once.
|
|
267
|
+
const lotSize = res.lotSize;
|
|
268
|
+
log(`[ARB] Lot size ${lotSize} on ${longEx} — recalculating (nothing was placed)`);
|
|
269
|
+
matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx, { lotSize });
|
|
270
|
+
if (!matched) {
|
|
271
|
+
log(`[ARB] Skip ${symbol}: can't meet lot size ${lotSize}`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
log(`[ARB] Retry: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}/leg)`);
|
|
275
|
+
res = await openPair(matched.size);
|
|
276
|
+
}
|
|
277
|
+
if (res.status === "quarantined") {
|
|
278
|
+
log(`[ARB] Entry ${symbol} QUARANTINED: ${res.reason} — exposure: ${res.exposure}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (res.status === "not_opened") {
|
|
282
|
+
log(`[ARB] Entry error ${symbol}: ${res.reason}${res.rolledBack !== null ? ` (long ${res.rolledBack} rolled back)` : ""}`);
|
|
283
|
+
continue;
|
|
272
284
|
}
|
|
285
|
+
if (res.trimmed !== "0")
|
|
286
|
+
log(`[ARB] Short filled ${res.hedged} — trimmed the long by ${res.trimmed} (reduce-only)`);
|
|
287
|
+
positions.push({
|
|
288
|
+
symbol,
|
|
289
|
+
longExchange: longEx,
|
|
290
|
+
shortExchange: shortEx,
|
|
291
|
+
size: res.hedged,
|
|
292
|
+
entrySpread: annualSpread,
|
|
293
|
+
openedAt: Date.now(),
|
|
294
|
+
entryPrices: { long: lowest.markPrice, short: highest.markPrice },
|
|
295
|
+
});
|
|
296
|
+
// Entry record: `arb status` / `arb close` confirm this pair from it (arbPairProvenance).
|
|
297
|
+
logExecution({
|
|
298
|
+
type: "arb_entry", exchange: `${longEx}+${shortEx}`, symbol, side: "entry", size: res.hedged,
|
|
299
|
+
status: "success", dryRun: false,
|
|
300
|
+
meta: { arbPairId: `${symbol.toUpperCase()}:${longEx}:${shortEx}`, longExchange: longEx, shortExchange: shortEx, spread: annualSpread, source: SOURCE },
|
|
301
|
+
});
|
|
302
|
+
log(`[ARB] OPENED ${symbol} delta-neutral ${res.hedged}/leg (${positions.length}/${maxPos})`);
|
|
273
303
|
}
|
|
274
304
|
}
|
|
275
305
|
}
|
|
@@ -314,23 +344,106 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
|
|
|
314
344
|
}
|
|
315
345
|
log(`[ARB] Funding rate arbitrage stopped. Positions: ${positions.length}`);
|
|
316
346
|
}
|
|
347
|
+
/** Venue position size string for one leg (abs, as the venue reports it), or null when flat. Read errors propagate. */
|
|
348
|
+
async function venueLegSize(adapter, symbol, side) {
|
|
349
|
+
invalidateCache("acct"); // the reduce-only close is sized from this read: never a cached position
|
|
350
|
+
const p = (await adapter.getPositions()).find(x => matchSymbol(x.symbol, symbol) && x.side === side);
|
|
351
|
+
if (p === undefined)
|
|
352
|
+
return null;
|
|
353
|
+
const abs = p.size.replace(/^-/, "");
|
|
354
|
+
return Number(abs) > 0 ? abs : null;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Close one tracked pair: reduce-only, verified, legs sequential (closePerpPairVerified), each
|
|
358
|
+
* sized from the venue's own position size string, capped at the tracked size (never rounded
|
|
359
|
+
* up, never another position's size). A leg already gone → the other leg alone (reduce-only,
|
|
360
|
+
* verified, one residual close). Unknown outcome / unhedged residual → quarantined. Returns
|
|
361
|
+
* true only when this pair is flat.
|
|
362
|
+
*/
|
|
363
|
+
async function closeArbPosition(pos, adapters, log) {
|
|
364
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
365
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
366
|
+
if (!longAdapter || !shortAdapter) {
|
|
367
|
+
log(`[ARB] Close ${pos.symbol}: adapter missing (${pos.longExchange}/${pos.shortExchange}) — left open`);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const cap = (venue) => (venue === null ? null : Number(venue) > Number(pos.size) ? pos.size : venue);
|
|
371
|
+
let longSize;
|
|
372
|
+
let shortSize;
|
|
373
|
+
try {
|
|
374
|
+
[longSize, shortSize] = await Promise.all([
|
|
375
|
+
venueLegSize(longAdapter, pos.symbol, "long").then(cap),
|
|
376
|
+
venueLegSize(shortAdapter, pos.symbol, "short").then(cap),
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
log(`[ARB] Close ${pos.symbol}: position read failed (${err instanceof Error ? err.message : String(err)}) — nothing sent, retry next cycle`);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (longSize === null && shortSize === null) {
|
|
384
|
+
log(`[ARB] ${pos.symbol}: both legs already flat — removing from tracking`);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (longSize !== null && shortSize !== null) {
|
|
388
|
+
const res = await closePerpPairVerified({
|
|
389
|
+
source: SOURCE, sym: pos.symbol,
|
|
390
|
+
longAdapter, longExch: pos.longExchange, longSym: pos.symbol, longSize,
|
|
391
|
+
shortAdapter, shortExch: pos.shortExchange, shortSym: pos.symbol, shortSize,
|
|
392
|
+
smart: false, log,
|
|
393
|
+
});
|
|
394
|
+
if (res.status === "closed") {
|
|
395
|
+
logExecution({
|
|
396
|
+
type: "arb_close", exchange: `${pos.longExchange}+${pos.shortExchange}`, symbol: pos.symbol, side: "close", size: pos.size,
|
|
397
|
+
status: "success", dryRun: false,
|
|
398
|
+
meta: { arbPairId: `${pos.symbol.toUpperCase()}:${pos.longExchange}:${pos.shortExchange}`, longExchange: pos.longExchange, shortExchange: pos.shortExchange, source: SOURCE },
|
|
399
|
+
});
|
|
400
|
+
log(`[ARB] CLOSED ${pos.symbol} — both legs unwound (long ${longSize}, short ${shortSize})`);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
if (res.status === "failed") {
|
|
404
|
+
log(`[ARB] Close error ${pos.symbol}: ${res.reason} — hedge intact`);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
log(`[ARB] Close ${pos.symbol} QUARANTINED: ${res.reason}`);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
// One leg is gone (liquidated / closed elsewhere): close the orphan leg alone.
|
|
411
|
+
const [adapter, venue, side, size, leg] = longSize !== null
|
|
412
|
+
? [longAdapter, pos.longExchange, "sell", longSize, "long"]
|
|
413
|
+
: [shortAdapter, pos.shortExchange, "buy", shortSize, "short"];
|
|
414
|
+
log(`[ARB] ${pos.symbol}: the other leg is gone — closing the orphaned ${leg} ${size} on ${venue} (reduce-only)`);
|
|
415
|
+
const q = (reason) => {
|
|
416
|
+
quarantineSymbol(SOURCE, pos.symbol, reason, `Check ${pos.symbol} on ${pos.longExchange} and ${pos.shortExchange}; close by hand, then: perp strategy quarantine-clear ${pos.symbol}`, log);
|
|
417
|
+
return false;
|
|
418
|
+
};
|
|
419
|
+
try {
|
|
420
|
+
const c = await closePerpVerified(adapter, venue, pos.symbol, side, size);
|
|
421
|
+
if (c.residual > 0)
|
|
422
|
+
return q(`orphaned ${leg} close on ${venue} closed ${c.closed} of ${size}, ${c.residual} still open${c.note !== null ? ` (${c.note})` : ""}`);
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (!isOutcomeUnknown(err) && isPositionGone(err))
|
|
426
|
+
return true;
|
|
427
|
+
return q(`orphaned ${leg} ${size} on ${venue}: close ${isOutcomeUnknown(err) ? "outcome unknown" : "failed"} (${err instanceof Error ? err.message : String(err)}) — unhedged`);
|
|
428
|
+
}
|
|
429
|
+
log(`[ARB] Closed orphaned ${leg} ${size} ${pos.symbol} on ${venue}`);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/** Emergency close of every tracked pair (verified, reduce-only). Returns the pairs still open. */
|
|
317
433
|
async function closeAllPositions(positions, adapters, log) {
|
|
434
|
+
const stillOpen = [];
|
|
318
435
|
for (const pos of positions) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
436
|
+
if (isQuarantined(pos.symbol, log)) {
|
|
437
|
+
log(`[ARB] ${pos.symbol} is quarantined — left open (perp strategy quarantine)`);
|
|
438
|
+
stillOpen.push(pos);
|
|
322
439
|
continue;
|
|
323
|
-
try {
|
|
324
|
-
await Promise.all([
|
|
325
|
-
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
326
|
-
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
327
|
-
]);
|
|
328
|
-
log(`[ARB] Emergency closed ${pos.symbol}`);
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
log(`[ARB] Failed to close ${pos.symbol}: ${err instanceof Error ? err.message : err}`);
|
|
332
440
|
}
|
|
441
|
+
if (await closeArbPosition(pos, adapters, log))
|
|
442
|
+
log(`[ARB] Emergency closed ${pos.symbol}`);
|
|
443
|
+
else
|
|
444
|
+
stillOpen.push(pos);
|
|
333
445
|
}
|
|
446
|
+
return stillOpen;
|
|
334
447
|
}
|
|
335
448
|
function sleep(ms) {
|
|
336
449
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -10,11 +10,32 @@ export interface GridParams {
|
|
|
10
10
|
intervalSec?: number;
|
|
11
11
|
maxRuntime?: number;
|
|
12
12
|
trailingStop?: number;
|
|
13
|
+
/**
|
|
14
|
+
* At stop (max runtime, trailing stop, SIGINT): close the grid's own inventory — the position
|
|
15
|
+
* change since the grid started — reduce-only, verified (closePerpVerified). Default false: the
|
|
16
|
+
* inventory is left open and reported (warning + result.inventory).
|
|
17
|
+
*/
|
|
18
|
+
closeOnStop?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** The grid's position change on `symbol` between start and stop (signed base: + long, − short). */
|
|
21
|
+
export interface GridInventory {
|
|
22
|
+
/** Signed position when the grid started. */
|
|
23
|
+
startSize: string;
|
|
24
|
+
/** Signed position after the remaining grid orders were cancelled; null = could not be read. */
|
|
25
|
+
endSize: string | null;
|
|
26
|
+
/** endSize − startSize (the grid's own net fills); null when endSize is unknown. */
|
|
27
|
+
gridDelta: string | null;
|
|
28
|
+
/** Base closed by --close-on-stop (0 when not requested / nothing to close). */
|
|
29
|
+
closed: string;
|
|
30
|
+
/** Inventory still open after stop (|gridDelta| − closed); null when unknown. */
|
|
31
|
+
leftOpen: string | null;
|
|
32
|
+
note: string | null;
|
|
13
33
|
}
|
|
14
34
|
export interface GridState {
|
|
15
35
|
gridLines: GridLine[];
|
|
16
36
|
activeOrders: Map<string, string>;
|
|
17
37
|
fills: number;
|
|
38
|
+
/** Estimated PnL of COMPLETED round trips only (grid step × size per take-profit fill). */
|
|
18
39
|
totalPnl: number;
|
|
19
40
|
peakEquity: number;
|
|
20
41
|
startedAt: number;
|
|
@@ -25,10 +46,20 @@ interface GridLine {
|
|
|
25
46
|
side: "buy" | "sell";
|
|
26
47
|
size: number;
|
|
27
48
|
filled: boolean;
|
|
49
|
+
/** The line's resting order is the take-profit leg of an earlier fill (its fill completes a round trip). */
|
|
50
|
+
flipped: boolean;
|
|
28
51
|
}
|
|
29
52
|
export declare function runGrid(adapter: ExchangeAdapter, params: GridParams, jobId?: string, log?: (msg: string) => void): Promise<{
|
|
30
53
|
fills: number;
|
|
31
54
|
totalPnl: number;
|
|
32
55
|
runtime: number;
|
|
56
|
+
inventory: GridInventory;
|
|
33
57
|
}>;
|
|
58
|
+
/**
|
|
59
|
+
* The grid's inventory at stop: position now − position at start. Without closeOnStop it is
|
|
60
|
+
* left open and reported (WARNING). With closeOnStop the part still held in the same direction is
|
|
61
|
+
* closed reduce-only (closePerpVerified); a delta the current position no longer holds (the
|
|
62
|
+
* position was reduced / flipped by something else) is not closed — closing it would open a position.
|
|
63
|
+
*/
|
|
64
|
+
export declare function settleGridInventory(adapter: ExchangeAdapter, symbol: string, startSize: string, closeOnStop: boolean, log: (msg: string) => void): Promise<GridInventory>;
|
|
34
65
|
export {};
|
package/dist/strategies/grid.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
1
2
|
import { updateJobState } from "../jobs.js";
|
|
3
|
+
import { closePerpVerified } from "../bot/perp-order.js";
|
|
4
|
+
import { formatComputedPrice } from "../exchanges/precision.js";
|
|
5
|
+
import { alignComputedLimitPrice } from "../bot/quote-rules.js";
|
|
6
|
+
import { symbolMatch } from "../utils.js";
|
|
7
|
+
import { venueRateString } from "../utils/numeric.js";
|
|
8
|
+
/** Signed position of `symbol` (+ long, − short, 0 none) as a decimal string. */
|
|
9
|
+
async function signedPosition(adapter, symbol) {
|
|
10
|
+
const p = (await adapter.getPositions()).find(x => symbolMatch(x.symbol, symbol));
|
|
11
|
+
if (p === undefined)
|
|
12
|
+
return "0";
|
|
13
|
+
const abs = p.size.replace(/^-/, "");
|
|
14
|
+
return p.side === "short" ? `-${abs}` : abs;
|
|
15
|
+
}
|
|
2
16
|
export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
17
|
+
assertCanClosePositions(adapter, `Grid on ${adapter.name}`);
|
|
3
18
|
const { symbol, upperPrice, lowerPrice, grids, totalSize } = params;
|
|
4
19
|
const intervalMs = (params.intervalSec ?? 10) * 1000;
|
|
5
20
|
const maxRuntime = (params.maxRuntime ?? 0) * 1000;
|
|
@@ -27,6 +42,7 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
27
42
|
side: "buy", // will be set based on current price
|
|
28
43
|
size: sizePerGrid,
|
|
29
44
|
filled: false,
|
|
45
|
+
flipped: false,
|
|
30
46
|
});
|
|
31
47
|
}
|
|
32
48
|
log(`[GRID] ${symbol} ${params.side} | ${grids} grids | $${lowerPrice} - $${upperPrice} | step $${step.toFixed(2)}`);
|
|
@@ -40,14 +56,17 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
40
56
|
startedAt: Date.now(),
|
|
41
57
|
running: true,
|
|
42
58
|
};
|
|
59
|
+
// Baseline position: the grid's inventory at stop is measured against it (a failed read stops here, nothing placed).
|
|
60
|
+
const startSize = await signedPosition(adapter, symbol);
|
|
43
61
|
// Graceful shutdown
|
|
44
62
|
const shutdown = () => { state.running = false; };
|
|
45
63
|
process.on("SIGINT", shutdown);
|
|
46
64
|
process.on("SIGTERM", shutdown);
|
|
65
|
+
let roundTrips = 0;
|
|
47
66
|
try {
|
|
48
67
|
// Get current price to determine buy/sell sides
|
|
49
68
|
const markets = await adapter.getMarkets();
|
|
50
|
-
const market = markets.find(m => m.symbol
|
|
69
|
+
const market = markets.find(m => symbolMatch(m.symbol, symbol));
|
|
51
70
|
const currentPrice = market ? parseFloat(market.markPrice) : (upperPrice + lowerPrice) / 2;
|
|
52
71
|
// Assign sides: buy below current, sell above current
|
|
53
72
|
for (const line of gridLines) {
|
|
@@ -84,6 +103,8 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
84
103
|
const line = gridLines[i];
|
|
85
104
|
state.fills++;
|
|
86
105
|
newFills++;
|
|
106
|
+
if (line.flipped)
|
|
107
|
+
roundTrips++;
|
|
87
108
|
line.filled = true;
|
|
88
109
|
// Flip side: buy → sell, sell → buy (take profit at next grid)
|
|
89
110
|
const newSide = line.side === "buy" ? "sell" : "buy";
|
|
@@ -92,12 +113,13 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
92
113
|
: line.price - step; // buy one grid below
|
|
93
114
|
if (newPrice >= lowerPrice && newPrice <= upperPrice) {
|
|
94
115
|
try {
|
|
95
|
-
const result = await adapter.limitOrder(symbol, newSide,
|
|
116
|
+
const result = await adapter.limitOrder(symbol, newSide, await alignComputedLimitPrice(adapter, symbol, newSide, formatComputedPrice(newPrice)), String(line.size));
|
|
96
117
|
const newOrderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
97
118
|
state.activeOrders.set(String(i), newOrderId);
|
|
98
119
|
line.side = newSide;
|
|
99
120
|
line.price = newPrice;
|
|
100
121
|
line.filled = false;
|
|
122
|
+
line.flipped = !line.flipped;
|
|
101
123
|
}
|
|
102
124
|
catch (err) {
|
|
103
125
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -110,9 +132,9 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
110
132
|
}
|
|
111
133
|
}
|
|
112
134
|
if (newFills > 0) {
|
|
113
|
-
// Estimate PnL from grid spacing
|
|
114
|
-
state.totalPnl
|
|
115
|
-
log(`[GRID] ${newFills} fill(s) | Total fills: ${state.fills} | Est. PnL: $${state.totalPnl
|
|
135
|
+
// Estimate PnL from grid spacing — completed round trips only (an opening fill earns nothing yet).
|
|
136
|
+
state.totalPnl = roundTrips > 0 ? Number(formatComputedPrice(roundTrips * step * sizePerGrid)) : 0;
|
|
137
|
+
log(`[GRID] ${newFills} fill(s) | Total fills: ${state.fills} | Round trips: ${roundTrips} | Est. PnL: $${state.totalPnl}`);
|
|
116
138
|
}
|
|
117
139
|
// Update job state
|
|
118
140
|
if (jobId) {
|
|
@@ -152,23 +174,79 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
|
152
174
|
try {
|
|
153
175
|
await adapter.cancelAllOrders(symbol);
|
|
154
176
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
177
|
+
catch (e) {
|
|
178
|
+
log(`[GRID] WARNING: cancelling the remaining ${symbol} orders failed (${e instanceof Error ? e.message : String(e)}) — check: perp account orders -e ${adapter.name}`);
|
|
157
179
|
}
|
|
158
180
|
}
|
|
181
|
+
const inventory = await settleGridInventory(adapter, symbol, startSize, params.closeOnStop === true, log);
|
|
159
182
|
const runtime = Math.floor((Date.now() - state.startedAt) / 1000);
|
|
160
|
-
log(`[GRID] Done. ${state.fills} fills, est. PnL $${state.totalPnl
|
|
183
|
+
log(`[GRID] Done. ${state.fills} fills (${roundTrips} round trips), est. PnL $${state.totalPnl}, runtime ${runtime}s`);
|
|
161
184
|
if (jobId) {
|
|
162
|
-
updateJobState(jobId, { status: "done", result: { fills: state.fills, totalPnl: state.totalPnl, runtime } });
|
|
185
|
+
updateJobState(jobId, { status: "done", result: { fills: state.fills, totalPnl: state.totalPnl, runtime, inventory } });
|
|
186
|
+
}
|
|
187
|
+
return { fills: state.fills, totalPnl: state.totalPnl, runtime, inventory };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The grid's inventory at stop: position now − position at start. Without closeOnStop it is
|
|
191
|
+
* left open and reported (WARNING). With closeOnStop the part still held in the same direction is
|
|
192
|
+
* closed reduce-only (closePerpVerified); a delta the current position no longer holds (the
|
|
193
|
+
* position was reduced / flipped by something else) is not closed — closing it would open a position.
|
|
194
|
+
*/
|
|
195
|
+
export async function settleGridInventory(adapter, symbol, startSize, closeOnStop, log) {
|
|
196
|
+
let endSize;
|
|
197
|
+
try {
|
|
198
|
+
endSize = await signedPosition(adapter, symbol);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
const note = `position could not be read after stop (${e instanceof Error ? e.message : String(e)})`;
|
|
202
|
+
log(`[GRID] WARNING: ${note} — the grid may have left a ${symbol} position open. Check: perp account positions -e ${adapter.name}`);
|
|
203
|
+
return { startSize, endSize: null, gridDelta: null, closed: "0", leftOpen: null, note };
|
|
204
|
+
}
|
|
205
|
+
const dec = Math.max((startSize.split(".")[1] ?? "").length, (endSize.split(".")[1] ?? "").length);
|
|
206
|
+
const delta = Number(venueRateString(Number(endSize) - Number(startSize), "grid inventory"));
|
|
207
|
+
const gridDelta = delta.toFixed(dec);
|
|
208
|
+
const base = { startSize, endSize, gridDelta };
|
|
209
|
+
if (delta === 0)
|
|
210
|
+
return { ...base, closed: "0", leftOpen: "0", note: null };
|
|
211
|
+
const deltaAbs = Math.abs(delta).toFixed(dec);
|
|
212
|
+
const dir = delta > 0 ? "long" : "short";
|
|
213
|
+
if (!closeOnStop) {
|
|
214
|
+
log(`[GRID] WARNING: the grid left ${deltaAbs} ${symbol} ${dir} open (position ${startSize} → ${endSize}). Close it: perp trade close ${symbol} -e ${adapter.name} (or rerun with --close-on-stop)`);
|
|
215
|
+
return { ...base, closed: "0", leftOpen: deltaAbs, note: "left open (no --close-on-stop)" };
|
|
216
|
+
}
|
|
217
|
+
const end = Number(endSize);
|
|
218
|
+
const held = Math.sign(end) === Math.sign(delta) ? Math.min(Math.abs(delta), Math.abs(end)) : 0;
|
|
219
|
+
if (held === 0) {
|
|
220
|
+
const note = `the position (${endSize}) no longer holds the grid's ${dir} inventory — not closed (it would open a position)`;
|
|
221
|
+
log(`[GRID] WARNING: ${note}. Check: perp account positions -e ${adapter.name}`);
|
|
222
|
+
return { ...base, closed: "0", leftOpen: deltaAbs, note };
|
|
223
|
+
}
|
|
224
|
+
const size = held.toFixed(dec);
|
|
225
|
+
log(`[GRID] --close-on-stop: closing ${size} ${symbol} ${dir} (reduce-only, verified)...`);
|
|
226
|
+
try {
|
|
227
|
+
const c = await closePerpVerified(adapter, adapter.name, symbol, delta > 0 ? "sell" : "buy", size, { decimals: dec });
|
|
228
|
+
const closed = c.closed.toFixed(dec);
|
|
229
|
+
const leftOpen = Math.max(0, Math.abs(delta) - c.closed).toFixed(dec);
|
|
230
|
+
if (c.residual > 0 || Number(leftOpen) > 0) {
|
|
231
|
+
log(`[GRID] WARNING: closed ${closed}; ${leftOpen} ${symbol} still open${c.note !== null ? ` (${c.note})` : ""}. Check: perp account positions -e ${adapter.name}`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
log(`[GRID] Closed the grid inventory (${closed} ${symbol}).`);
|
|
235
|
+
}
|
|
236
|
+
return { ...base, closed, leftOpen, note: c.note };
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
const note = `close failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
240
|
+
log(`[GRID] WARNING: ${note} — ${deltaAbs} ${symbol} ${dir} may still be open. Check: perp account positions -e ${adapter.name}`);
|
|
241
|
+
return { ...base, closed: "0", leftOpen: null, note };
|
|
163
242
|
}
|
|
164
|
-
return { fills: state.fills, totalPnl: state.totalPnl, runtime };
|
|
165
243
|
}
|
|
166
244
|
async function placeGridOrders(adapter, symbol, gridLines, state, log) {
|
|
167
245
|
let placed = 0;
|
|
168
246
|
for (let i = 0; i < gridLines.length; i++) {
|
|
169
247
|
const line = gridLines[i];
|
|
170
248
|
try {
|
|
171
|
-
const result = await adapter.limitOrder(symbol, line.side,
|
|
249
|
+
const result = await adapter.limitOrder(symbol, line.side, await alignComputedLimitPrice(adapter, symbol, line.side, formatComputedPrice(line.price)), String(line.size));
|
|
172
250
|
const orderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
173
251
|
state.activeOrders.set(String(i), orderId);
|
|
174
252
|
placed++;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
2
|
+
import { PerpError } from "../errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* An order of a DCA / TWAP run whose outcome or fill size is unknown: it is NEVER resubmitted.
|
|
5
|
+
* The symbol is quarantined (every automated path skips it until a human clears it) and the
|
|
6
|
+
* returned ORDER_PENDING error ends the run.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pendingStop(source: "dca" | "twap", adapter: ExchangeAdapter, symbol: string, reason: string, log: (msg: string) => void): PerpError;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PerpError } from "../errors.js";
|
|
2
|
+
import { quarantineSymbol } from "../bot/quarantine.js";
|
|
3
|
+
/**
|
|
4
|
+
* An order of a DCA / TWAP run whose outcome or fill size is unknown: it is NEVER resubmitted.
|
|
5
|
+
* The symbol is quarantined (every automated path skips it until a human clears it) and the
|
|
6
|
+
* returned ORDER_PENDING error ends the run.
|
|
7
|
+
*/
|
|
8
|
+
export function pendingStop(source, adapter, symbol, reason, log) {
|
|
9
|
+
const remediation = `Check ${symbol} on ${adapter.name} (perp -e ${adapter.name} account positions; perp -e ${adapter.name} account orders) — do NOT resubmit; fix by hand, then: perp strategy quarantine-clear ${symbol}`;
|
|
10
|
+
quarantineSymbol(source, symbol, `${reason} — not resubmitted`, remediation, log);
|
|
11
|
+
return new PerpError("ORDER_PENDING", `${reason} — run stopped, ${symbol} quarantined`, { exchange: adapter.name, remediation });
|
|
12
|
+
}
|