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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position quarantine (design §12.3) — GLOBAL per symbol.
|
|
3
|
+
*
|
|
4
|
+
* A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
|
|
5
|
+
* confirmed, a close whose post-trade verification failed, or a position left unhedged
|
|
6
|
+
* by a failed rollback must NOT be retried automatically: a retry can double-buy,
|
|
7
|
+
* double-sell or open a naked perp. Such a symbol is quarantined here, persisted in
|
|
8
|
+
* ~/.perp/bot-quarantine.json, and skipped by EVERY automated path — the bot strategies
|
|
9
|
+
* (funding-arb, funding-arb-v2, spot-perp-arb, funding-auto), `perp arb exec` (perp-perp
|
|
10
|
+
* and spot:<venue>) and the `perp arb auto` / `arb dex-auto` daemons — no entry, no close,
|
|
11
|
+
* no rotation — until a human clears it (`perp arb close` refuses it without --force):
|
|
12
|
+
*
|
|
13
|
+
* perp strategy quarantine # list
|
|
14
|
+
* perp strategy quarantine-clear <symbol> [--strategy <source>]
|
|
15
|
+
*
|
|
16
|
+
* Entries are stored per source ("<source>:<KEY>", source = strategy name or CLI path)
|
|
17
|
+
* but CHECKED by the canonical key alone (quarantineKey: HIP-3 dex prefix and "-PERP"
|
|
18
|
+
* stripped, DENOMINATION_ALIASES applied, uppercased — "xyz:TSLA" ≡ "TSLA", "KPEPE" ≡
|
|
19
|
+
* "1000PEPE"): one venue's unknown outcome blocks the underlying for all strategies,
|
|
20
|
+
* venues and processes. `entry.symbol` keeps the symbol as the source reported it. The file is re-read on every check, so a clear
|
|
21
|
+
* from the CLI takes effect in running bots on their next tick (no restart). Only an
|
|
22
|
+
* entry whose persist failed is held in process memory (logged CRITICAL); it holds
|
|
23
|
+
* until that process stops.
|
|
24
|
+
*
|
|
25
|
+
* Network scope: an entry records the network it was set on (`network`); a process that called
|
|
26
|
+
* setQuarantineNetwork(n) sees only entries of n plus entries of unknown network (null — set by
|
|
27
|
+
* a process that never declared its network: they apply to EVERY network, fail closed). Entries
|
|
28
|
+
* written before network scoping have no tag and are MAINNET (like the arb state's legacy file).
|
|
29
|
+
* A process that never declared its network sees every entry (fail closed). Mainnet / unknown
|
|
30
|
+
* entries keep the legacy key "<source>:<KEY>"; testnet ones are keyed "testnet:<source>:<KEY>".
|
|
31
|
+
* Every load → modify → save of the file holds the cross-process lock (withFileLock).
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
34
|
+
import { resolve } from "path";
|
|
35
|
+
import { PerpError } from "../errors.js";
|
|
36
|
+
import { DENOMINATION_ALIASES } from "../exchanges/canonical-symbols.js";
|
|
37
|
+
import { withFileLock } from "../arb/file-lock.js";
|
|
38
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
39
|
+
export const QUARANTINE_FILE = resolve(PERP_DIR, "bot-quarantine.json");
|
|
40
|
+
/** Entries whose persist failed, by "<source>:<KEY>" (this process only). */
|
|
41
|
+
const unpersisted = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Canonical quarantine key for a symbol — the ONLY way a symbol is compared here (set,
|
|
44
|
+
* check, clear): uppercased, HIP-3 dex prefix ("xyz:") and "-PERP" suffix stripped,
|
|
45
|
+
* DENOMINATION_ALIASES applied (KPEPE → 1000PEPE, UBTC → BTC).
|
|
46
|
+
*/
|
|
47
|
+
export function quarantineKey(symbol) {
|
|
48
|
+
let up = symbol.trim().toUpperCase();
|
|
49
|
+
const colon = up.lastIndexOf(":");
|
|
50
|
+
if (colon >= 0)
|
|
51
|
+
up = up.slice(colon + 1);
|
|
52
|
+
up = up.replace(/-PERP$/, "");
|
|
53
|
+
return DENOMINATION_ALIASES[up] ?? up;
|
|
54
|
+
}
|
|
55
|
+
/** The network this process trades on (setQuarantineNetwork); null = not declared → every entry applies. */
|
|
56
|
+
let activeNetwork = null;
|
|
57
|
+
/** Declare the network this process trades on: entries are set on it and checked against it. */
|
|
58
|
+
export function setQuarantineNetwork(network) {
|
|
59
|
+
activeNetwork = network;
|
|
60
|
+
}
|
|
61
|
+
/** Test hook: back to "network not declared". */
|
|
62
|
+
export function resetQuarantineNetwork() {
|
|
63
|
+
activeNetwork = null;
|
|
64
|
+
}
|
|
65
|
+
function keyOf(source, symbol, network) {
|
|
66
|
+
return `${network === "testnet" ? "testnet:" : ""}${source}:${quarantineKey(symbol)}`;
|
|
67
|
+
}
|
|
68
|
+
/** Does `e` block on this process's network? (unknown on either side → yes, fail closed) */
|
|
69
|
+
function applies(e) {
|
|
70
|
+
return activeNetwork === null || e.network === null || e.network === activeNetwork;
|
|
71
|
+
}
|
|
72
|
+
/** Read the file. A missing file = nothing quarantined; an unreadable / malformed file throws (never "empty"). */
|
|
73
|
+
function load() {
|
|
74
|
+
if (!existsSync(QUARANTINE_FILE))
|
|
75
|
+
return { version: 1, entries: {} };
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(readFileSync(QUARANTINE_FILE, "utf-8"));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
throw new PerpError("INVALID_PARAMS", `Bot quarantine file is unreadable: ${QUARANTINE_FILE} (${e instanceof Error ? e.message : String(e)})`, {
|
|
82
|
+
remediation: `Fix or remove ${QUARANTINE_FILE} after verifying every quarantined position on the venues`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const f = parsed;
|
|
86
|
+
if (f === null || f.version !== 1 || typeof f.entries !== "object" || f.entries === null) {
|
|
87
|
+
throw new PerpError("INVALID_PARAMS", `Bot quarantine file has an unexpected shape: ${QUARANTINE_FILE}`, {
|
|
88
|
+
remediation: `Fix or remove ${QUARANTINE_FILE} after verifying every quarantined position on the venues`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Legacy entries (written before network scoping) have no tag: they are mainnet.
|
|
92
|
+
for (const e of Object.values(f.entries)) {
|
|
93
|
+
if (e.network === undefined)
|
|
94
|
+
e.network = "mainnet";
|
|
95
|
+
}
|
|
96
|
+
return f;
|
|
97
|
+
}
|
|
98
|
+
function withQuarantineLock(fn) {
|
|
99
|
+
return withFileLock(QUARANTINE_FILE, "Bot quarantine file", fn);
|
|
100
|
+
}
|
|
101
|
+
function save(f) {
|
|
102
|
+
if (!existsSync(PERP_DIR))
|
|
103
|
+
mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
|
|
104
|
+
const tmp = QUARANTINE_FILE + ".tmp";
|
|
105
|
+
writeFileSync(tmp, JSON.stringify(f, null, 2), { mode: 0o600 });
|
|
106
|
+
renameSync(tmp, QUARANTINE_FILE);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Quarantine `symbol` (for every strategy / CLI path) and log CRITICAL with the
|
|
110
|
+
* remediation. `source` records who did it. If persisting fails the entry is held in
|
|
111
|
+
* this process's memory and the failure is logged loudly.
|
|
112
|
+
*/
|
|
113
|
+
export function quarantineSymbol(source, symbol, reason, remediation, log) {
|
|
114
|
+
const sym = symbol.toUpperCase();
|
|
115
|
+
const key = quarantineKey(sym);
|
|
116
|
+
const entry = { strategy: source, symbol: sym, reason, remediation, time: new Date().toISOString(), network: activeNetwork };
|
|
117
|
+
const k = keyOf(source, sym, activeNetwork);
|
|
118
|
+
log(` [QUARANTINE] CRITICAL: ${sym}${key !== sym ? ` (key ${key})` : ""} quarantined by ${source} — ${reason}`);
|
|
119
|
+
log(` [QUARANTINE] Remediation: ${remediation}`);
|
|
120
|
+
log(` [QUARANTINE] No bot / arb path will touch ${key} until cleared: perp strategy quarantine-clear ${key}`);
|
|
121
|
+
try {
|
|
122
|
+
withQuarantineLock(() => {
|
|
123
|
+
const f = load();
|
|
124
|
+
// Never narrow: an existing unknown-network entry under this key keeps applying everywhere.
|
|
125
|
+
if (f.entries[k]?.network === null)
|
|
126
|
+
entry.network = null;
|
|
127
|
+
f.entries[k] = entry;
|
|
128
|
+
save(f);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
unpersisted.set(k, entry);
|
|
133
|
+
log(` [QUARANTINE] CRITICAL: could not persist quarantine to ${QUARANTINE_FILE} (${e instanceof Error ? e.message : String(e)}) — it holds only until this process stops`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The quarantine entry blocking `symbol` (any source), or null. Re-reads the file on
|
|
138
|
+
* every call. An unreadable quarantine file fails CLOSED (a synthetic entry, logged) —
|
|
139
|
+
* never "not quarantined".
|
|
140
|
+
*/
|
|
141
|
+
export function quarantineEntryFor(symbol, log) {
|
|
142
|
+
const sym = symbol.toUpperCase();
|
|
143
|
+
const key = quarantineKey(sym);
|
|
144
|
+
for (const e of unpersisted.values())
|
|
145
|
+
if (quarantineKey(e.symbol) === key && applies(e))
|
|
146
|
+
return e;
|
|
147
|
+
try {
|
|
148
|
+
for (const e of Object.values(load().entries))
|
|
149
|
+
if (quarantineKey(e.symbol) === key && applies(e))
|
|
150
|
+
return e;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
155
|
+
log(` [QUARANTINE] CRITICAL: ${msg} — treating ${sym} as quarantined`);
|
|
156
|
+
return { strategy: "quarantine-file", symbol: sym, reason: msg, remediation: `Fix or remove ${QUARANTINE_FILE}`, time: new Date().toISOString(), network: activeNetwork };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** true when `symbol` is quarantined by any strategy / CLI path (see quarantineEntryFor). */
|
|
160
|
+
export function isQuarantined(symbol, log) {
|
|
161
|
+
return quarantineEntryFor(symbol, log) !== null;
|
|
162
|
+
}
|
|
163
|
+
/** Entries that apply on this process's network (every entry when it never declared one). */
|
|
164
|
+
export function listQuarantine() {
|
|
165
|
+
return [...Object.values(load().entries), ...unpersisted.values()].filter(applies);
|
|
166
|
+
}
|
|
167
|
+
/** Remove entries (that apply on this process's network) whose quarantineKey matches symbol's (one source, or every source when null). Returns the removed entries. */
|
|
168
|
+
export function clearQuarantine(symbol, strategy) {
|
|
169
|
+
return withQuarantineLock(() => {
|
|
170
|
+
const f = load();
|
|
171
|
+
const key = quarantineKey(symbol);
|
|
172
|
+
const removed = [];
|
|
173
|
+
const match = (e) => quarantineKey(e.symbol) === key && (strategy === null || e.strategy === strategy) && applies(e);
|
|
174
|
+
for (const [k, e] of Object.entries(f.entries)) {
|
|
175
|
+
if (match(e)) {
|
|
176
|
+
removed.push(e);
|
|
177
|
+
delete f.entries[k];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const [k, e] of unpersisted) {
|
|
181
|
+
if (match(e)) {
|
|
182
|
+
removed.push(e);
|
|
183
|
+
unpersisted.delete(k);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (removed.length > 0)
|
|
187
|
+
save(f);
|
|
188
|
+
return removed;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface QuoteRules {
|
|
3
|
+
/** Market tick; null = the venue publishes none on this data path (the adapter itself rounds / validates, e.g. HL). */
|
|
4
|
+
tickSize: string | null;
|
|
5
|
+
stepSize: string;
|
|
6
|
+
minSize: string | null;
|
|
7
|
+
minNotional: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** The market's tick / lot / minimums (venue public market list + the adapter's lot step). Failed read → throws. */
|
|
10
|
+
export declare function quoteRulesOf(adapter: ExchangeAdapter, symbol: string): Promise<QuoteRules>;
|
|
11
|
+
/** Limit quote on the venue grid: price aligned passively (buy ↓, sell ↑), size floored to the lot, minimums checked. */
|
|
12
|
+
export declare function prepareLimitQuote(rules: QuoteRules, venue: string, symbol: string, side: "buy" | "sell", price: string, size: string): {
|
|
13
|
+
price: string;
|
|
14
|
+
size: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A COMPUTED limit price aligned to the venue's own price grid when the venue publishes no tick
|
|
18
|
+
* (Hyperliquid: 5 significant figures / szDecimals rule, `alignComputedPrice`), passively for
|
|
19
|
+
* `side` (buy ↓, sell ↑). Venues with a tick are already aligned by the callers; returned as is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function alignComputedLimitPrice(adapter: ExchangeAdapter, symbol: string, side: "buy" | "sell", price: string): Promise<string>;
|
|
22
|
+
/** Market order size floored to the lot; the minimum order value is checked against `refPrice` (null = size minimum only). */
|
|
23
|
+
export declare function prepareMarketSize(rules: QuoteRules, venue: string, symbol: string, size: string, refPrice: number | null): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Venue order rules for bot quotes (engine executeActions): every limit price is aligned to the
|
|
3
|
+
* market tick in the passive direction (bid DOWN, ask UP — never more aggressive than the
|
|
4
|
+
* strategy asked), every size floored to the lot, and an order under the venue's published
|
|
5
|
+
* minimum size / order value is refused before sending (SIZE_TOO_SMALL).
|
|
6
|
+
*
|
|
7
|
+
* Live 2026-10-12 (Aster SOL, tick 0.01): simple-mm / avellaneda-mm / grid-mm / liquidation-mm /
|
|
8
|
+
* regime-mm / rfq-agent quoted "108.861840" and never placed an order (requireOnTick refusal);
|
|
9
|
+
* engine-mm on Pacifica was refused by the venue ("not a multiple of tick size 0.01").
|
|
10
|
+
*/
|
|
11
|
+
import { PerpError } from "../errors.js";
|
|
12
|
+
import { adapterNetworkOf } from "../exchanges/adapter-network.js";
|
|
13
|
+
import { alignProtectionPrice, floorSize, lotStepOf } from "../exchanges/precision.js";
|
|
14
|
+
import { getVenue } from "../exchanges/venue.js";
|
|
15
|
+
import { symbolMatch } from "../utils.js";
|
|
16
|
+
/** The market's tick / lot / minimums (venue public market list + the adapter's lot step). Failed read → throws. */
|
|
17
|
+
export async function quoteRulesOf(adapter, symbol) {
|
|
18
|
+
const network = adapterNetworkOf(adapter);
|
|
19
|
+
if (network === null) {
|
|
20
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name}: the adapter's network is unknown, so the ${symbol} tick / minimums cannot be read; nothing was sent`, { exchange: adapter.name });
|
|
21
|
+
}
|
|
22
|
+
const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network), lotStepOf(adapter, symbol)]);
|
|
23
|
+
const vm = markets.find(m => symbolMatch(m.symbol, symbol));
|
|
24
|
+
return { tickSize: vm?.tickSize ?? null, stepSize, minSize: vm?.minSize ?? null, minNotional: vm?.minNotional ?? null };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A plain decimal for a strategy-computed number (strategies pass toFixed() strings; an exponent
|
|
28
|
+
* form is refused by the decimal helpers).
|
|
29
|
+
*/
|
|
30
|
+
function plain(v, what) {
|
|
31
|
+
if (/^\d+(\.\d+)?$/.test(v))
|
|
32
|
+
return v;
|
|
33
|
+
const n = Number(v);
|
|
34
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
35
|
+
throw new PerpError("INVALID_PARAMS", `${what} ${v} is not a positive number; nothing was sent`);
|
|
36
|
+
return n.toFixed(12).replace(/\.?0+$/, "");
|
|
37
|
+
}
|
|
38
|
+
/** Limit quote on the venue grid: price aligned passively (buy ↓, sell ↑), size floored to the lot, minimums checked. */
|
|
39
|
+
export function prepareLimitQuote(rules, venue, symbol, side, price, size) {
|
|
40
|
+
const px = rules.tickSize === null ? plain(price, "price") : alignProtectionPrice(plain(price, "price"), rules.tickSize, side === "buy" ? "sell" : "buy");
|
|
41
|
+
const sz = floorSize(plain(size, "size"), rules.stepSize);
|
|
42
|
+
requireMinimums(rules, venue, symbol, sz, px);
|
|
43
|
+
return { price: px, size: sz };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A COMPUTED limit price aligned to the venue's own price grid when the venue publishes no tick
|
|
47
|
+
* (Hyperliquid: 5 significant figures / szDecimals rule, `alignComputedPrice`), passively for
|
|
48
|
+
* `side` (buy ↓, sell ↑). Venues with a tick are already aligned by the callers; returned as is.
|
|
49
|
+
*/
|
|
50
|
+
export async function alignComputedLimitPrice(adapter, symbol, side, price) {
|
|
51
|
+
const a = adapter;
|
|
52
|
+
return typeof a.alignComputedPrice === "function" ? a.alignComputedPrice(symbol, side, price) : price;
|
|
53
|
+
}
|
|
54
|
+
/** Market order size floored to the lot; the minimum order value is checked against `refPrice` (null = size minimum only). */
|
|
55
|
+
export function prepareMarketSize(rules, venue, symbol, size, refPrice) {
|
|
56
|
+
const sz = floorSize(plain(size, "size"), rules.stepSize);
|
|
57
|
+
requireMinimums(rules, venue, symbol, sz, refPrice !== null && refPrice > 0 ? String(refPrice) : null);
|
|
58
|
+
return sz;
|
|
59
|
+
}
|
|
60
|
+
function requireMinimums(rules, venue, symbol, size, price) {
|
|
61
|
+
if (rules.minSize !== null && Number(size) < Number(rules.minSize)) {
|
|
62
|
+
throw new PerpError("SIZE_TOO_SMALL", `${venue} ${symbol}: size ${size} is below the venue minimum ${rules.minSize}; nothing was sent`, { exchange: venue, minSize: rules.minSize });
|
|
63
|
+
}
|
|
64
|
+
if (price !== null && rules.minNotional !== null && Number(size) * Number(price) < Number(rules.minNotional)) {
|
|
65
|
+
throw new PerpError("SIZE_TOO_SMALL", `${venue} ${symbol}: ${size} @ ${price} = $${(Number(size) * Number(price)).toFixed(2)} is below the venue minimum order value $${rules.minNotional}; nothing was sent`, {
|
|
66
|
+
exchange: venue, minNotional: rules.minNotional, remediation: "Raise the strategy's order size",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Quotes farther than this from mid are refused (an error, never placed): a guard against a
|
|
4
|
+
* mis-scaled γ / κ / σ (live 2026-10-12: SOL ≈ $109 was quoted at $38.62 — the spread term
|
|
5
|
+
* (2/γ)·ln(1+γ/κ) = 1.29 was multiplied by the mid as if it were a fraction).
|
|
6
|
+
*/
|
|
7
|
+
export declare const AVELLANEDA_MAX_QUOTE_DISTANCE = 0.05;
|
|
8
|
+
/**
|
|
9
|
+
* Avellaneda–Stoikov (2008) in absolute price units, with T − t in seconds:
|
|
10
|
+
* r = s − q·γ·σ²·(T − t)
|
|
11
|
+
* δ = γ·σ²·(T − t) + (2/γ)·ln(1 + γ/κ) (total bid–ask spread)
|
|
12
|
+
* bid = r − δ/2, ask = r + δ/2
|
|
13
|
+
* σ in $/√s, γ and κ in 1/$ — every term is then in $. Throws when a quote is more than
|
|
14
|
+
* AVELLANEDA_MAX_QUOTE_DISTANCE from mid or not positive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function avellanedaQuotes(o: {
|
|
17
|
+
mid: number;
|
|
18
|
+
q: number;
|
|
19
|
+
gamma: number;
|
|
20
|
+
kappa: number;
|
|
21
|
+
sigma: number;
|
|
22
|
+
secondsLeft: number;
|
|
23
|
+
}): {
|
|
24
|
+
reservation: number;
|
|
25
|
+
spread: number;
|
|
26
|
+
bid: number;
|
|
27
|
+
ask: number;
|
|
28
|
+
};
|
|
2
29
|
export declare class AvellanedaMmStrategy implements Strategy {
|
|
3
30
|
readonly name = "avellaneda-mm";
|
|
4
31
|
describe(): {
|
|
@@ -22,6 +49,9 @@ export declare class AvellanedaMmStrategy implements Strategy {
|
|
|
22
49
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
23
50
|
onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
24
51
|
onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
25
|
-
/** Estimate sigma from ATR or use override */
|
|
26
|
-
private estimateSigma;
|
|
27
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* σ in $/√s: the override, else ATR(14) of the snapshot candles (in $ per candle interval)
|
|
55
|
+
* ÷ √(interval in seconds, from the candle timestamps). Too few candles → throws (never a constant).
|
|
56
|
+
*/
|
|
57
|
+
export declare function estimateSigma(snapshot: Pick<EnrichedSnapshot, "klines">, sigmaOverride: number): number;
|
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
2
|
import { calculateATR } from "../indicators.js";
|
|
3
|
+
import { PerpError } from "../../errors.js";
|
|
4
|
+
import { symbolMatch } from "../../utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* Quotes farther than this from mid are refused (an error, never placed): a guard against a
|
|
7
|
+
* mis-scaled γ / κ / σ (live 2026-10-12: SOL ≈ $109 was quoted at $38.62 — the spread term
|
|
8
|
+
* (2/γ)·ln(1+γ/κ) = 1.29 was multiplied by the mid as if it were a fraction).
|
|
9
|
+
*/
|
|
10
|
+
export const AVELLANEDA_MAX_QUOTE_DISTANCE = 0.05;
|
|
11
|
+
/**
|
|
12
|
+
* Avellaneda–Stoikov (2008) in absolute price units, with T − t in seconds:
|
|
13
|
+
* r = s − q·γ·σ²·(T − t)
|
|
14
|
+
* δ = γ·σ²·(T − t) + (2/γ)·ln(1 + γ/κ) (total bid–ask spread)
|
|
15
|
+
* bid = r − δ/2, ask = r + δ/2
|
|
16
|
+
* σ in $/√s, γ and κ in 1/$ — every term is then in $. Throws when a quote is more than
|
|
17
|
+
* AVELLANEDA_MAX_QUOTE_DISTANCE from mid or not positive.
|
|
18
|
+
*/
|
|
19
|
+
export function avellanedaQuotes(o) {
|
|
20
|
+
const { mid, q, gamma, kappa, sigma, secondsLeft } = o;
|
|
21
|
+
for (const [k, v] of Object.entries({ mid, gamma, kappa, sigma, secondsLeft })) {
|
|
22
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
23
|
+
throw new PerpError("INVALID_PARAMS", `avellaneda-mm: ${k} must be a positive number (got ${v})`);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isFinite(q))
|
|
26
|
+
throw new PerpError("INVALID_PARAMS", `avellaneda-mm: inventory is not a number (${q})`);
|
|
27
|
+
const risk = gamma * sigma ** 2 * secondsLeft;
|
|
28
|
+
const reservation = mid - q * risk;
|
|
29
|
+
const spread = risk + (2 / gamma) * Math.log(1 + gamma / kappa);
|
|
30
|
+
const bid = reservation - spread / 2;
|
|
31
|
+
const ask = reservation + spread / 2;
|
|
32
|
+
const far = Math.max(Math.abs(mid - bid), Math.abs(ask - mid)) / mid;
|
|
33
|
+
if (!(bid > 0) || far > AVELLANEDA_MAX_QUOTE_DISTANCE) {
|
|
34
|
+
throw new PerpError("INVALID_PARAMS", `avellaneda-mm: quotes ${bid.toFixed(6)} / ${ask.toFixed(6)} are ${(far * 100).toFixed(1)}% from mid ${mid} (limit ${AVELLANEDA_MAX_QUOTE_DISTANCE * 100}%) — γ=${gamma}, κ=${kappa} (per $), σ=${sigma.toPrecision(4)} $/√s, T−t=${secondsLeft.toFixed(0)}s, q=${q}; nothing was placed`, {
|
|
35
|
+
remediation: "γ and κ are per price unit: raise kappa / lower gamma for a tighter spread, or shorten --param timeHorizon",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { reservation, spread, bid, ask };
|
|
39
|
+
}
|
|
3
40
|
export class AvellanedaMmStrategy {
|
|
4
41
|
name = "avellaneda-mm";
|
|
5
42
|
describe() {
|
|
6
43
|
return {
|
|
7
|
-
description: "Avellaneda-Stoikov optimal market making with inventory-adjusted reservation price",
|
|
44
|
+
description: "Avellaneda-Stoikov optimal market making with inventory-adjusted reservation price (absolute price units; quotes > 5% from mid are refused)",
|
|
8
45
|
params: [
|
|
9
|
-
{ name: "gamma", type: "number", required: false, default: 0.1, description: "Risk aversion
|
|
10
|
-
{ name: "kappa", type: "number", required: false, default: 1.5, description: "Order
|
|
11
|
-
{ name: "sigma", type: "number", required: false, default: 0, description: "Volatility
|
|
46
|
+
{ name: "gamma", type: "number", required: false, default: 0.1, description: "Risk aversion γ, per price unit (1/$)" },
|
|
47
|
+
{ name: "kappa", type: "number", required: false, default: 1.5, description: "Order-book liquidity κ, per price unit (1/$)" },
|
|
48
|
+
{ name: "sigma", type: "number", required: false, default: 0, description: "Volatility in $ per √second (0 = estimate from the candles' ATR)" },
|
|
12
49
|
{ name: "maxPosition", type: "number", required: true, description: "Maximum position size" },
|
|
13
|
-
{ name: "timeHorizon", type: "number", required: false, default: 86400, description: "Session length in seconds" },
|
|
50
|
+
{ name: "timeHorizon", type: "number", required: false, default: 86400, description: "Session length T in seconds" },
|
|
14
51
|
{ name: "size", type: "number", required: true, description: "Order size per level" },
|
|
15
52
|
],
|
|
16
53
|
};
|
|
@@ -21,91 +58,56 @@ export class AvellanedaMmStrategy {
|
|
|
21
58
|
_config = {};
|
|
22
59
|
async init(ctx, _snapshot) {
|
|
23
60
|
this._config = ctx.config;
|
|
24
|
-
ctx.state.set("inventory", 0);
|
|
25
61
|
ctx.state.set("sessionStart", Date.now());
|
|
26
62
|
const p = this.params;
|
|
27
63
|
ctx.log(` [AVELLANEDA-MM] gamma=${p.gamma}, kappa=${p.kappa}, sigma=${p.sigma || "auto"}, maxPos=${p.maxPosition}, T=${p.timeHorizon}s`);
|
|
28
64
|
}
|
|
29
65
|
async onTick(ctx, snapshot) {
|
|
30
66
|
const params = this.params;
|
|
31
|
-
const actions = [];
|
|
32
|
-
actions.push({ type: "cancel_all" });
|
|
67
|
+
const actions = [{ type: "cancel_all" }];
|
|
33
68
|
const gamma = params.gamma || 0.1;
|
|
34
69
|
const kappa = params.kappa || 1.5;
|
|
35
70
|
const mid = snapshot.price;
|
|
36
|
-
|
|
37
|
-
const sigma = this.estimateSigma(snapshot, params.sigma);
|
|
38
|
-
// Time remaining: fraction of session left
|
|
71
|
+
const sigma = estimateSigma(snapshot, params.sigma);
|
|
39
72
|
const sessionStart = ctx.state.get("sessionStart") ?? Date.now();
|
|
40
73
|
const elapsed = (Date.now() - sessionStart) / 1000;
|
|
41
74
|
const timeHorizon = params.timeHorizon || 86400;
|
|
42
|
-
const
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const reservationPrice = mid - q * gamma * (sigma ** 2) * timeRemaining;
|
|
48
|
-
// Optimal spread:
|
|
49
|
-
// delta = gamma * sigma^2 * (T-t) + (2/gamma) * ln(1 + gamma/kappa)
|
|
50
|
-
const optimalSpread = gamma * (sigma ** 2) * timeRemaining + (2 / gamma) * Math.log(1 + gamma / kappa);
|
|
51
|
-
const spreadInPrice = optimalSpread * mid; // convert to price terms
|
|
52
|
-
const bidPrice = reservationPrice - spreadInPrice / 2;
|
|
53
|
-
const askPrice = reservationPrice + spreadInPrice / 2;
|
|
54
|
-
// Clamp to max position: skip orders that would exceed
|
|
75
|
+
const secondsLeft = Math.max(timeHorizon - elapsed, 1);
|
|
76
|
+
// Inventory q: the venue position (signed base), never an estimate.
|
|
77
|
+
const pos = (await ctx.adapter.getPositions()).find(p => symbolMatch(p.symbol, ctx.symbol));
|
|
78
|
+
const q = pos === undefined ? 0 : (pos.side === "short" ? -1 : 1) * Math.abs(Number(pos.size));
|
|
79
|
+
const { reservation, spread, bid, ask } = avellanedaQuotes({ mid, q, gamma, kappa, sigma, secondsLeft });
|
|
55
80
|
const size = params.size;
|
|
56
|
-
if (q < params.maxPosition
|
|
57
|
-
actions.push({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
price: bidPrice.toFixed(6),
|
|
61
|
-
size: String(size),
|
|
62
|
-
orderType: "limit",
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
if (q > -params.maxPosition && askPrice > 0) {
|
|
66
|
-
actions.push({
|
|
67
|
-
type: "place_order",
|
|
68
|
-
side: "sell",
|
|
69
|
-
price: askPrice.toFixed(6),
|
|
70
|
-
size: String(size),
|
|
71
|
-
orderType: "limit",
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
// Update inventory estimate
|
|
75
|
-
const prevMid = ctx.state.get("prevMid") ?? mid;
|
|
76
|
-
if (mid > prevMid && q < params.maxPosition) {
|
|
77
|
-
ctx.state.set("inventory", q + size * 0.5);
|
|
78
|
-
}
|
|
79
|
-
else if (mid < prevMid && q > -params.maxPosition) {
|
|
80
|
-
ctx.state.set("inventory", q - size * 0.5);
|
|
81
|
-
}
|
|
82
|
-
ctx.state.set("prevMid", mid);
|
|
81
|
+
if (q < params.maxPosition)
|
|
82
|
+
actions.push({ type: "place_order", side: "buy", price: bid.toFixed(8), size: String(size), orderType: "limit" });
|
|
83
|
+
if (q > -params.maxPosition)
|
|
84
|
+
actions.push({ type: "place_order", side: "sell", price: ask.toFixed(8), size: String(size), orderType: "limit" });
|
|
83
85
|
if (ctx.tick % 10 === 0) {
|
|
84
|
-
ctx.log(` [AVELLANEDA-MM] r=$${
|
|
86
|
+
ctx.log(` [AVELLANEDA-MM] mid=$${mid}, r=$${reservation.toFixed(4)}, spread=$${spread.toFixed(4)}, q=${q}, sigma=${sigma.toPrecision(4)} $/√s, T-t=${secondsLeft.toFixed(0)}s`);
|
|
85
87
|
}
|
|
86
88
|
return actions;
|
|
87
89
|
}
|
|
88
90
|
async onStop(_ctx) {
|
|
89
91
|
return [{ type: "cancel_all" }];
|
|
90
92
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return snapshot.volatility24h / 100 || 0.02;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* σ in $/√s: the override, else ATR(14) of the snapshot candles (in $ per candle interval)
|
|
96
|
+
* ÷ √(interval in seconds, from the candle timestamps). Too few candles → throws (never a constant).
|
|
97
|
+
*/
|
|
98
|
+
export function estimateSigma(snapshot, sigmaOverride) {
|
|
99
|
+
if (sigmaOverride > 0)
|
|
100
|
+
return sigmaOverride;
|
|
101
|
+
const k = [...snapshot.klines].sort((a, b) => a.time - b.time);
|
|
102
|
+
if (k.length < 15) {
|
|
103
|
+
throw new PerpError("PRICE_STALE", `avellaneda-mm: ${k.length} candles — at least 15 are needed to estimate σ (or pass --param sigma=<$ per √s>)`);
|
|
104
|
+
}
|
|
105
|
+
const atr = calculateATR(k.map(c => ({ high: parseFloat(c.high), low: parseFloat(c.low), close: parseFloat(c.close) })), 14);
|
|
106
|
+
const intervalSec = (k[k.length - 1].time - k[0].time) / (k.length - 1) / 1000;
|
|
107
|
+
const last = atr[atr.length - 1];
|
|
108
|
+
if (!(intervalSec > 0) || !(last > 0)) {
|
|
109
|
+
throw new PerpError("PRICE_STALE", `avellaneda-mm: σ cannot be estimated (ATR ${last}, candle interval ${intervalSec}s)`);
|
|
109
110
|
}
|
|
111
|
+
return last / Math.sqrt(intervalSec);
|
|
110
112
|
}
|
|
111
113
|
registerStrategy("avellaneda-mm", (_config) => new AvellanedaMmStrategy());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
|
+
import { assertFundingArbLegs } from "./funding-arb-utils.js";
|
|
2
3
|
export class BasisArbStrategy {
|
|
3
4
|
name = "basis-arb";
|
|
4
5
|
_config = {};
|
|
@@ -16,6 +17,7 @@ export class BasisArbStrategy {
|
|
|
16
17
|
get size() { return Number(this._config.size ?? 0.1); }
|
|
17
18
|
get holdingPeriodHours() { return Number(this._config.holdingPeriodHours ?? 24); }
|
|
18
19
|
async init(ctx, _snapshot) {
|
|
20
|
+
assertFundingArbLegs(ctx);
|
|
19
21
|
this._config = ctx.config;
|
|
20
22
|
ctx.state.set("position", null);
|
|
21
23
|
ctx.log(` [BASIS] Ready | threshold=${this.annualizedBasisThreshold}% holdingPeriod=${this.holdingPeriodHours}h size=${this.size}`);
|
|
@@ -21,5 +21,7 @@ export declare class DCAStrategy implements Strategy {
|
|
|
21
21
|
private _config;
|
|
22
22
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
23
23
|
onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
24
|
+
/** Done once the --orders total has been placed (0 = unlimited, never done). */
|
|
25
|
+
isDone(ctx: StrategyContext): boolean;
|
|
24
26
|
onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
25
27
|
}
|
|
@@ -56,6 +56,11 @@ export class DCAStrategy {
|
|
|
56
56
|
orderType: "market",
|
|
57
57
|
}];
|
|
58
58
|
}
|
|
59
|
+
/** Done once the --orders total has been placed (0 = unlimited, never done). */
|
|
60
|
+
isDone(ctx) {
|
|
61
|
+
const total = this.params.total_orders;
|
|
62
|
+
return total > 0 && ctx.state.get("dcaOrdersPlaced") >= total;
|
|
63
|
+
}
|
|
59
64
|
async onStop(_ctx) {
|
|
60
65
|
return [{ type: "cancel_all" }];
|
|
61
66
|
}
|
|
@@ -26,12 +26,52 @@ export declare class FundingArbStrategy implements Strategy {
|
|
|
26
26
|
private get params();
|
|
27
27
|
private _config;
|
|
28
28
|
private _failCooldown;
|
|
29
|
+
/** Consecutive failed closes per symbol, spot-perp and perp-perp (quarantined at MAX_CLOSE_FAILURES). */
|
|
30
|
+
private _closeFailures;
|
|
31
|
+
/** Venues whose positions could not be read during recovery: no entries there until re-read. */
|
|
32
|
+
private _unrecovered;
|
|
33
|
+
private executor;
|
|
34
|
+
/** Symbols this bot entered or tracked this run — the only symbols its stop cancels orders on. */
|
|
35
|
+
private _traded;
|
|
36
|
+
private quarantined;
|
|
37
|
+
/** Unknown / unhedged outcome: stop touching this symbol until a human clears it. */
|
|
38
|
+
private quarantine;
|
|
29
39
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Rebuild tracked positions from the venues (perp-perp pairs + same-venue spot-perp
|
|
42
|
+
* hedges). A venue whose positions or spot balances cannot be read is returned in
|
|
43
|
+
* `unreadable` — never treated as holding nothing.
|
|
44
|
+
*/
|
|
45
|
+
private recoverPositions;
|
|
46
|
+
/**
|
|
47
|
+
* true when `symbol` has a live perp position on any of `venues` that the bot does not track
|
|
48
|
+
* (a manual / foreign position): no entry is stacked on it. A failed read counts as held
|
|
49
|
+
* (unknown is never "flat").
|
|
50
|
+
*/
|
|
51
|
+
private heldUntracked;
|
|
30
52
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
31
53
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
54
|
+
/**
|
|
55
|
+
* Close a perp-perp position: long then short, both reduce-only and verified by the
|
|
56
|
+
* position delta. Returns true when both legs are closed (or already gone).
|
|
57
|
+
* - an unknown leg outcome → quarantine (never retried / rolled back blindly);
|
|
58
|
+
* - long close definitely failed → hedge intact, cooldown; MAX_CLOSE_FAILURES in a row →
|
|
59
|
+
* quarantine (no endless retry);
|
|
60
|
+
* - short close definitely failed after THIS call closed the long → quarantine at once
|
|
61
|
+
* (naked short). The long is never re-opened (a non-reduce-only order that could loop
|
|
62
|
+
* close → fail → re-open every tick), same as closePerpPairVerified;
|
|
63
|
+
* - short close failed with the long already gone → one more reduce-only close of the
|
|
64
|
+
* orphaned short; if that fails → quarantine.
|
|
65
|
+
*/
|
|
66
|
+
private closePerpPerp;
|
|
67
|
+
/**
|
|
68
|
+
* Close a spot-perp position via SpotPerpExecutor.closePosition (perp short FIRST,
|
|
69
|
+
* reduce-only and verified; spot sold only after; a perp already gone is detected by
|
|
70
|
+
* the reduce-only rejection, never by selling spot again). Returns true only when
|
|
71
|
+
* fully closed. Unknown / unhedged outcomes quarantine the symbol; a plain failure
|
|
72
|
+
* cools down, and MAX_CLOSE_FAILURES consecutive failures quarantine it — each attempt
|
|
73
|
+
* can trade (perp close → spot sell fail → re-hedge), so it never loops.
|
|
74
|
+
*/
|
|
35
75
|
private closeSpotPerp;
|
|
36
76
|
/** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
|
|
37
77
|
private openSpotPerp;
|