perp-cli 0.12.15 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -31
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +74 -84
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +33 -16
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/dist/arb/state.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
1
|
+
import { copyFileSync, constants as fsConstants, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
+
import { PerpError } from "../errors.js";
|
|
4
|
+
import { withFileLock } from "./file-lock.js";
|
|
5
|
+
/**
|
|
6
|
+
* Arb daemon state is PER NETWORK (review N2): a testnet run must never read, close, or
|
|
7
|
+
* overwrite mainnet positions, and vice versa.
|
|
8
|
+
*
|
|
9
|
+
* mainnet → ~/.perp/arb-state.json (the pre-network path, unchanged)
|
|
10
|
+
* testnet → ~/.perp/arb-state.testnet.json
|
|
11
|
+
*
|
|
12
|
+
* Every saved file carries `network`; a load whose tag does not match the requested
|
|
13
|
+
* network throws. Files written before this change have no tag. They are MAINNET by
|
|
14
|
+
* definition (mainnet was — and is — the documented default network, and the mainnet
|
|
15
|
+
* path is unchanged), so a mainnet load adopts an untagged file and tags it in place
|
|
16
|
+
* (atomic tmp+rename, after a one-time backup copy `arb-state.json.pre-network.bak`).
|
|
17
|
+
* The history does NOT prove an untagged file could only come from mainnet (`-n testnet`
|
|
18
|
+
* routed `arb exec/auto` to testnet adapters since 2026-03 and they wrote the same file),
|
|
19
|
+
* so a TESTNET load refuses while an untagged legacy file exists — the user decides
|
|
20
|
+
* (adopt as mainnet, or move it to the testnet path). Nothing is ever dropped or copied
|
|
21
|
+
* into both networks.
|
|
22
|
+
*/
|
|
3
23
|
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
4
24
|
const STATE_FILE = resolve(PERP_DIR, "arb-state.json");
|
|
5
|
-
// Allow overriding the state file path for testing
|
|
25
|
+
// Allow overriding the (mainnet) state file path for testing; testnet derives from it.
|
|
6
26
|
let stateFilePath = STATE_FILE;
|
|
7
27
|
export function setStateFilePath(path) {
|
|
8
28
|
stateFilePath = path;
|
|
@@ -10,13 +30,73 @@ export function setStateFilePath(path) {
|
|
|
10
30
|
export function resetStateFilePath() {
|
|
11
31
|
stateFilePath = STATE_FILE;
|
|
12
32
|
}
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
/** Absolute state-file path for `network` (mainnet = the pre-network path). */
|
|
34
|
+
export function arbStateFilePath(network) {
|
|
35
|
+
if (network === "mainnet")
|
|
36
|
+
return stateFilePath;
|
|
37
|
+
return stateFilePath.endsWith(".json") ? `${stateFilePath.slice(0, -5)}.testnet.json` : `${stateFilePath}.testnet`;
|
|
38
|
+
}
|
|
39
|
+
function ensureDir(path) {
|
|
40
|
+
const dir = resolve(path, "..");
|
|
15
41
|
if (!existsSync(dir))
|
|
16
42
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
17
43
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
44
|
+
/** Parse a state file's tag without the .tmp recovery (used for the legacy-file guard). */
|
|
45
|
+
function readTag(path) {
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
return null;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
throw new PerpError("INVALID_PARAMS", `Arb state ${path} is not valid JSON (${e instanceof Error ? e.message : String(e)}) — refusing to guess its network`, {
|
|
54
|
+
remediation: `Inspect or restore ${path} (a .tmp / .pre-network.bak copy may exist next to it)`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { untagged: parsed.network === undefined };
|
|
58
|
+
}
|
|
59
|
+
/** Load the daemon state for `network`. Falls back to .tmp if main file is corrupted. */
|
|
60
|
+
export function loadArbState(network, opts = { readOnly: false }) {
|
|
61
|
+
if (network === "testnet") {
|
|
62
|
+
const legacy = readTag(arbStateFilePath("mainnet"));
|
|
63
|
+
if (legacy?.untagged) {
|
|
64
|
+
const main = arbStateFilePath("mainnet");
|
|
65
|
+
throw new PerpError("INVALID_PARAMS", `Untagged arb state ${main} (written before per-network state) exists — refusing to guess its network on testnet`, {
|
|
66
|
+
remediation: `If it holds MAINNET positions (the default network), adopt it once with any mainnet arb read: perp -n mainnet arb status. `
|
|
67
|
+
+ `If it holds TESTNET positions, move it: mv ${main} ${arbStateFilePath("testnet")}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const state = loadArbStateFile(arbStateFilePath(network));
|
|
72
|
+
if (state === null)
|
|
73
|
+
return null;
|
|
74
|
+
if (state.network === undefined) {
|
|
75
|
+
// Only reachable for mainnet (testnet refused above / its path never held untagged data).
|
|
76
|
+
// A read-only (dry-run) load tags the copy in memory and leaves the file untouched.
|
|
77
|
+
if (opts.readOnly)
|
|
78
|
+
state.network = "mainnet";
|
|
79
|
+
else
|
|
80
|
+
adoptLegacyAsMainnet(state);
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
if (state.network !== network) {
|
|
84
|
+
throw new PerpError("INVALID_PARAMS", `Arb state ${arbStateFilePath(network)} is tagged ${state.network}, not ${network}`, {
|
|
85
|
+
remediation: `The file was moved or edited by hand. Restore it to ${arbStateFilePath(state.network)}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
/** One-time adoption of a pre-network (untagged) state file as mainnet: backup copy, then atomic in-place tag. */
|
|
91
|
+
function adoptLegacyAsMainnet(state) {
|
|
92
|
+
const path = arbStateFilePath("mainnet");
|
|
93
|
+
const backup = `${path}.pre-network.bak`;
|
|
94
|
+
if (!existsSync(backup))
|
|
95
|
+
copyFileSync(path, backup, fsConstants.COPYFILE_EXCL);
|
|
96
|
+
state.network = "mainnet";
|
|
97
|
+
writeStateAtomic(path, state);
|
|
98
|
+
}
|
|
99
|
+
function loadArbStateFile(stateFilePath) {
|
|
20
100
|
const tmpPath = stateFilePath + ".tmp";
|
|
21
101
|
// Try main file first
|
|
22
102
|
if (existsSync(stateFilePath)) {
|
|
@@ -55,47 +135,110 @@ export function loadArbState() {
|
|
|
55
135
|
* Writes to .tmp first, then renames to main file.
|
|
56
136
|
* If crash occurs mid-write, .tmp is partial but main file is intact.
|
|
57
137
|
*/
|
|
58
|
-
export function saveArbState(state) {
|
|
59
|
-
|
|
60
|
-
|
|
138
|
+
export function saveArbState(network, state) {
|
|
139
|
+
if (state.network !== undefined && state.network !== network) {
|
|
140
|
+
throw new PerpError("INVALID_PARAMS", `Refusing to save ${state.network} arb state as ${network}`, {
|
|
141
|
+
remediation: "This is a bug — the state object was loaded for another network. Re-run the command.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Same network on the -n testnet path: never let a testnet save land while an untagged legacy file is unresolved.
|
|
145
|
+
if (network === "testnet")
|
|
146
|
+
loadArbState("testnet");
|
|
147
|
+
writeStateAtomic(arbStateFilePath(network), { ...state, network });
|
|
148
|
+
}
|
|
149
|
+
function writeStateAtomic(path, state) {
|
|
150
|
+
ensureDir(path);
|
|
151
|
+
const tmpPath = path + ".tmp";
|
|
61
152
|
const json = JSON.stringify(state, null, 2);
|
|
62
153
|
writeFileSync(tmpPath, json, { mode: 0o600 });
|
|
63
|
-
renameSync(tmpPath,
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
154
|
+
renameSync(tmpPath, path);
|
|
155
|
+
}
|
|
156
|
+
/** Identity of a state entry: symbol + long venue + short venue + mode (absent mode = perp-perp). */
|
|
157
|
+
export function positionKey(p) {
|
|
158
|
+
return `${p.symbol.toUpperCase()}|${p.longExchange}|${p.shortExchange}|${p.mode ?? "perp-perp"}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Put `pos` into `positions`: an entry with the same key is replaced (a re-record of the same
|
|
162
|
+
* pair); an entry of the same symbol on OTHER venues / another mode is never overwritten —
|
|
163
|
+
* RISK_VIOLATION (the state tracks one pair per symbol; overwriting would drop a live pair).
|
|
164
|
+
*/
|
|
165
|
+
export function upsertPosition(positions, pos) {
|
|
166
|
+
const key = positionKey(pos);
|
|
167
|
+
const other = positions.find(p => p.symbol.toUpperCase() === pos.symbol.toUpperCase() && positionKey(p) !== key);
|
|
168
|
+
if (other !== undefined) {
|
|
169
|
+
throw new PerpError("RISK_VIOLATION", `Arb state already tracks ${other.symbol} as ${other.mode ?? "perp-perp"} long ${other.longExchange} / short ${other.shortExchange} — refusing to overwrite it with ${pos.mode ?? "perp-perp"} long ${pos.longExchange} / short ${pos.shortExchange}`, {
|
|
170
|
+
remediation: `Both pairs may be open: check ${pos.symbol} on ${[...new Set([other.longExchange, other.shortExchange, pos.longExchange, pos.shortExchange])].join(", ")} (perp arb status), close one by hand, then record / drop it (perp arb close ${pos.symbol} --state-only)`,
|
|
171
|
+
});
|
|
71
172
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
173
|
+
return [...positions.filter(p => positionKey(p) !== key), pos];
|
|
174
|
+
}
|
|
175
|
+
/** Load → modify → save of the state for `network` under the cross-process lock. */
|
|
176
|
+
function withStateLock(network, fn) {
|
|
177
|
+
return withFileLock(arbStateFilePath(network), `Arb state (${network})`, fn);
|
|
178
|
+
}
|
|
179
|
+
/** Add a position to the persisted state for `network`. Auto-initializes state if missing. Throws RISK_VIOLATION for a conflicting entry (upsertPosition). */
|
|
180
|
+
export function addPosition(network, pos) {
|
|
181
|
+
withStateLock(network, () => {
|
|
182
|
+
let state = loadArbState(network);
|
|
183
|
+
if (!state) {
|
|
184
|
+
// State was lost — create minimal valid state to avoid losing position tracking
|
|
185
|
+
state = createInitialState({ minSpread: 0, closeSpread: 0, size: 0, holdDays: 0, bridgeCost: 0, maxPositions: 10, settleStrategy: "off" });
|
|
186
|
+
}
|
|
187
|
+
state.positions = upsertPosition(state.positions, pos);
|
|
188
|
+
saveArbState(network, state);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** Remove a position by symbol from the persisted state for `network` (one entry per symbol: upsertPosition). */
|
|
192
|
+
export function removePosition(network, symbol) {
|
|
193
|
+
withStateLock(network, () => {
|
|
194
|
+
const state = loadArbState(network);
|
|
195
|
+
if (!state)
|
|
196
|
+
return;
|
|
197
|
+
state.positions = state.positions.filter(p => p.symbol !== symbol);
|
|
198
|
+
saveArbState(network, state);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** Remove exactly the entry with `entry`'s key (symbol + venues + mode); another pair of the symbol is left alone. */
|
|
202
|
+
export function removePositionEntry(network, entry) {
|
|
203
|
+
withStateLock(network, () => {
|
|
204
|
+
const state = loadArbState(network);
|
|
205
|
+
if (!state)
|
|
206
|
+
return;
|
|
207
|
+
const key = positionKey(entry);
|
|
208
|
+
state.positions = state.positions.filter(p => positionKey(p) !== key);
|
|
209
|
+
saveArbState(network, state);
|
|
210
|
+
});
|
|
84
211
|
}
|
|
85
212
|
/** Update a position by symbol with partial updates. */
|
|
86
|
-
export function updatePosition(symbol, updates) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
213
|
+
export function updatePosition(network, symbol, updates) {
|
|
214
|
+
withStateLock(network, () => {
|
|
215
|
+
const state = loadArbState(network);
|
|
216
|
+
if (!state)
|
|
217
|
+
return;
|
|
218
|
+
const idx = state.positions.findIndex(p => p.symbol === symbol);
|
|
219
|
+
if (idx === -1)
|
|
220
|
+
return;
|
|
221
|
+
state.positions[idx] = { ...state.positions[idx], ...updates };
|
|
222
|
+
saveArbState(network, state);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** Update exactly the entry with `entry`'s key (symbol + venues + mode). */
|
|
226
|
+
export function updatePositionEntry(network, entry, updates) {
|
|
227
|
+
withStateLock(network, () => {
|
|
228
|
+
const state = loadArbState(network);
|
|
229
|
+
if (!state)
|
|
230
|
+
return;
|
|
231
|
+
const key = positionKey(entry);
|
|
232
|
+
const idx = state.positions.findIndex(p => positionKey(p) === key);
|
|
233
|
+
if (idx === -1)
|
|
234
|
+
return;
|
|
235
|
+
state.positions[idx] = { ...state.positions[idx], ...updates };
|
|
236
|
+
saveArbState(network, state);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** Get all persisted positions for `network`. */
|
|
240
|
+
export function getPositions(network) {
|
|
241
|
+
const state = loadArbState(network);
|
|
99
242
|
if (!state)
|
|
100
243
|
return [];
|
|
101
244
|
return state.positions;
|
|
@@ -111,3 +254,32 @@ export function createInitialState(config) {
|
|
|
111
254
|
config,
|
|
112
255
|
};
|
|
113
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Live: the per-network state file. Dry-run (review N3): an in-memory copy seeded READ-ONLY
|
|
259
|
+
* from the real state — nothing is ever written to disk, so a simulated entry is never picked
|
|
260
|
+
* up by a live run and a simulated close never drops a real position from tracking.
|
|
261
|
+
*/
|
|
262
|
+
export function createDaemonStateStore(network, dryRun, config) {
|
|
263
|
+
if (!dryRun) {
|
|
264
|
+
return {
|
|
265
|
+
load: () => loadArbState(network),
|
|
266
|
+
save: (next) => saveArbState(network, next),
|
|
267
|
+
add: (pos) => addPosition(network, pos),
|
|
268
|
+
remove: (symbol) => removePosition(network, symbol),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
let sim = structuredClone(loadArbState(network, { readOnly: true }));
|
|
272
|
+
return {
|
|
273
|
+
load: () => structuredClone(sim),
|
|
274
|
+
save: (next) => { sim = structuredClone(next); },
|
|
275
|
+
add: (pos) => {
|
|
276
|
+
const next = sim ?? createInitialState(config);
|
|
277
|
+
next.positions = upsertPosition(next.positions, structuredClone(pos));
|
|
278
|
+
sim = next;
|
|
279
|
+
},
|
|
280
|
+
remove: (symbol) => {
|
|
281
|
+
if (sim !== null)
|
|
282
|
+
sim.positions = sim.positions.filter(p => p.symbol !== symbol);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
package/dist/arb/utils.d.ts
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Arb utilities: settlement timing strategy, basis risk monitoring, and webhook notifications.
|
|
3
3
|
*/
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Settlement schedules per venue (UTC hours when settlement occurs), generated
|
|
6
|
+
* from `VenueDefinition.settlementIntervalHours`, anchored at 00:00 UTC
|
|
7
|
+
* (design §10.1, §10.13 #1). Venues whose interval is per-market (Aster)
|
|
8
|
+
* have no entry — pass the market's interval to getLastSettlement().
|
|
9
|
+
*/
|
|
5
10
|
export declare const SETTLEMENT_SCHEDULES: Record<string, number[]>;
|
|
6
11
|
export type SettleStrategy = "block" | "aggressive" | "off";
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* Settlement interval in hours for a venue ref. `marketIntervalHours` (the
|
|
14
|
+
* market's `settlementIntervalHours`) takes precedence; it is required for
|
|
15
|
+
* per-market venues. Unknown venues throw UNKNOWN_VENUE.
|
|
16
|
+
*/
|
|
17
|
+
export declare function settlementIntervalHours(exchange: string, marketIntervalHours?: number | null): number;
|
|
18
|
+
/**
|
|
19
|
+
* Get the most recent settlement time for an exchange (anchored at 00:00 UTC).
|
|
20
|
+
* @param marketIntervalHours market settlement interval; required for per-market venues
|
|
21
|
+
* @returns Date of the most recent past settlement (== now when exactly on a settlement)
|
|
10
22
|
*/
|
|
11
|
-
export declare function getLastSettlement(exchange: string, now?: Date): Date;
|
|
23
|
+
export declare function getLastSettlement(exchange: string, now?: Date, marketIntervalHours?: number | null): Date;
|
|
12
24
|
/**
|
|
13
25
|
* Get minutes since the most recent settlement for an exchange.
|
|
14
26
|
*/
|
|
15
|
-
export declare function getMinutesSinceSettlement(exchange: string, now?: Date): number;
|
|
27
|
+
export declare function getMinutesSinceSettlement(exchange: string, now?: Date, marketIntervalHours?: number | null): number;
|
|
16
28
|
/**
|
|
17
29
|
* Compute a score boost for aggressive settlement mode.
|
|
18
30
|
* Returns a multiplier > 1.0 if within the post-settlement window, else 1.0.
|
|
19
31
|
* The boost decays linearly from 1.5x (right after settlement) to 1.0x (at windowMinutes).
|
|
32
|
+
* `marketIntervalFor` returns the market's settlement interval per exchange;
|
|
33
|
+
* it must return a number for per-market venues (Aster).
|
|
20
34
|
*/
|
|
21
|
-
export declare function aggressiveSettleBoost(longExchange: string, shortExchange: string, windowMinutes?: number, now?: Date): number;
|
|
35
|
+
export declare function aggressiveSettleBoost(longExchange: string, shortExchange: string, windowMinutes?: number, now?: Date, marketIntervalFor?: (exchange: string) => number | null): number;
|
|
22
36
|
/**
|
|
23
37
|
* Estimate cumulative funding between now and the next settlement.
|
|
24
38
|
*
|
package/dist/arb/utils.js
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Arb utilities: settlement timing strategy, basis risk monitoring, and webhook notifications.
|
|
3
3
|
*/
|
|
4
|
+
import { PerpError } from "../errors.js";
|
|
5
|
+
import { listVenues, settlementIntervalOf } from "../exchanges/venue.js";
|
|
6
|
+
import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
|
|
4
7
|
// ── Settlement Timing Strategy ──
|
|
5
|
-
|
|
6
|
-
export const SETTLEMENT_SCHEDULES = {
|
|
7
|
-
hyperliquid: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
8
|
-
pacifica: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
9
|
-
lighter: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
10
|
-
aster: [0, 8, 16], // every 8 hours (Binance convention)
|
|
11
|
-
};
|
|
8
|
+
ensureVenuesRegistered();
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Settlement schedules per venue (UTC hours when settlement occurs), generated
|
|
11
|
+
* from `VenueDefinition.settlementIntervalHours`, anchored at 00:00 UTC
|
|
12
|
+
* (design §10.1, §10.13 #1). Venues whose interval is per-market (Aster)
|
|
13
|
+
* have no entry — pass the market's interval to getLastSettlement().
|
|
15
14
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export const SETTLEMENT_SCHEDULES = Object.fromEntries(listVenues()
|
|
16
|
+
.filter(v => v.settlementIntervalHours !== "perMarket")
|
|
17
|
+
.map(v => [v.id, scheduleFor(v.settlementIntervalHours)]));
|
|
18
|
+
function scheduleFor(intervalHours) {
|
|
19
|
+
if (!(intervalHours > 0) || 24 % intervalHours !== 0) {
|
|
20
|
+
throw new PerpError("INVALID_PARAMS", `Settlement interval ${intervalHours}h does not divide a UTC day`);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Exactly at settlement time
|
|
33
|
-
if (hour === currentHour && currentMinutes === 0 && currentSeconds === 0) {
|
|
34
|
-
const last = new Date(now);
|
|
35
|
-
last.setUTCHours(hour, 0, 0, 0);
|
|
36
|
-
return last;
|
|
22
|
+
return Array.from({ length: 24 / intervalHours }, (_, i) => i * intervalHours);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Settlement interval in hours for a venue ref. `marketIntervalHours` (the
|
|
26
|
+
* market's `settlementIntervalHours`) takes precedence; it is required for
|
|
27
|
+
* per-market venues. Unknown venues throw UNKNOWN_VENUE.
|
|
28
|
+
*/
|
|
29
|
+
export function settlementIntervalHours(exchange, marketIntervalHours = null) {
|
|
30
|
+
if (marketIntervalHours !== null) {
|
|
31
|
+
if (!(marketIntervalHours > 0)) {
|
|
32
|
+
throw new PerpError("INVALID_PARAMS", `Invalid settlement interval ${marketIntervalHours}h for ${exchange}`);
|
|
37
33
|
}
|
|
34
|
+
return marketIntervalHours;
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
return settlementIntervalOf(exchange);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the most recent settlement time for an exchange (anchored at 00:00 UTC).
|
|
40
|
+
* @param marketIntervalHours market settlement interval; required for per-market venues
|
|
41
|
+
* @returns Date of the most recent past settlement (== now when exactly on a settlement)
|
|
42
|
+
*/
|
|
43
|
+
export function getLastSettlement(exchange, now = new Date(), marketIntervalHours = null) {
|
|
44
|
+
const intervalMs = settlementIntervalHours(exchange, marketIntervalHours) * 3_600_000;
|
|
45
|
+
return new Date(Math.floor(now.getTime() / intervalMs) * intervalMs);
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Get minutes since the most recent settlement for an exchange.
|
|
47
49
|
*/
|
|
48
|
-
export function getMinutesSinceSettlement(exchange, now = new Date()) {
|
|
49
|
-
const lastSettle = getLastSettlement(exchange, now);
|
|
50
|
+
export function getMinutesSinceSettlement(exchange, now = new Date(), marketIntervalHours = null) {
|
|
51
|
+
const lastSettle = getLastSettlement(exchange, now, marketIntervalHours);
|
|
50
52
|
return (now.getTime() - lastSettle.getTime()) / (1000 * 60);
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Compute a score boost for aggressive settlement mode.
|
|
54
56
|
* Returns a multiplier > 1.0 if within the post-settlement window, else 1.0.
|
|
55
57
|
* The boost decays linearly from 1.5x (right after settlement) to 1.0x (at windowMinutes).
|
|
58
|
+
* `marketIntervalFor` returns the market's settlement interval per exchange;
|
|
59
|
+
* it must return a number for per-market venues (Aster).
|
|
56
60
|
*/
|
|
57
|
-
export function aggressiveSettleBoost(longExchange, shortExchange, windowMinutes = 10, now = new Date()) {
|
|
58
|
-
const longMinutes = getMinutesSinceSettlement(longExchange, now);
|
|
59
|
-
const shortMinutes = getMinutesSinceSettlement(shortExchange, now);
|
|
61
|
+
export function aggressiveSettleBoost(longExchange, shortExchange, windowMinutes = 10, now = new Date(), marketIntervalFor = () => null) {
|
|
62
|
+
const longMinutes = getMinutesSinceSettlement(longExchange, now, marketIntervalFor(longExchange));
|
|
63
|
+
const shortMinutes = getMinutesSinceSettlement(shortExchange, now, marketIntervalFor(shortExchange));
|
|
60
64
|
// Use the minimum of the two — we want BOTH exchanges to have recently settled
|
|
61
65
|
const minMinutes = Math.min(longMinutes, shortMinutes);
|
|
62
66
|
if (minMinutes <= windowMinutes) {
|
package/dist/bot/conditions.d.ts
CHANGED
|
@@ -19,14 +19,14 @@ export interface MarketSnapshot {
|
|
|
19
19
|
export declare function calculateRSI(closes: number[], period?: number): number;
|
|
20
20
|
export declare function getMarketSnapshot(adapter: ExchangeAdapter, symbol: string): Promise<MarketSnapshot>;
|
|
21
21
|
export declare function evaluateCondition(condition: Condition, snapshot: MarketSnapshot, context: {
|
|
22
|
-
equity: number;
|
|
22
|
+
equity: number | null;
|
|
23
23
|
startTime: number;
|
|
24
|
-
peakEquity: number;
|
|
24
|
+
peakEquity: number | null;
|
|
25
25
|
dailyPnl: number;
|
|
26
26
|
}): boolean;
|
|
27
27
|
export declare function evaluateAllConditions(conditions: Condition[], snapshot: MarketSnapshot, context: {
|
|
28
|
-
equity: number;
|
|
28
|
+
equity: number | null;
|
|
29
29
|
startTime: number;
|
|
30
|
-
peakEquity: number;
|
|
30
|
+
peakEquity: number | null;
|
|
31
31
|
dailyPnl: number;
|
|
32
32
|
}, mode?: "all" | "any"): boolean;
|
package/dist/bot/conditions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { symbolMatch } from "../utils.js";
|
|
1
2
|
// ── RSI Calculation (Wilder smoothing) ──
|
|
2
3
|
/**
|
|
3
4
|
* Calculate RSI from an array of closing prices using Wilder's smoothing method.
|
|
@@ -39,13 +40,16 @@ export function calculateRSI(closes, period = 14) {
|
|
|
39
40
|
}
|
|
40
41
|
export async function getMarketSnapshot(adapter, symbol) {
|
|
41
42
|
const markets = await adapter.getMarkets();
|
|
42
|
-
const m = markets.find(mk => mk.symbol
|
|
43
|
+
const m = markets.find(mk => symbolMatch(mk.symbol, symbol));
|
|
43
44
|
if (!m) {
|
|
44
45
|
throw new Error(`Market "${symbol}" not found on ${adapter.name}`);
|
|
45
46
|
}
|
|
46
47
|
const price = parseFloat(m.markPrice);
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Unknown funding (venue publishes none, e.g. QFEX) → NaN, never 0: a 0 would satisfy
|
|
49
|
+
// "funding below X" conditions on a rate nobody reported. NaN makes every comparison false.
|
|
50
|
+
const fundingRate = m.fundingRate === null ? NaN : parseFloat(m.fundingRate);
|
|
51
|
+
// Unknown volume → NaN, never 0: a 0 would read as "no liquidity" to volume conditions.
|
|
52
|
+
const volume24h = m.volume24h === null ? NaN : parseFloat(m.volume24h);
|
|
49
53
|
// Try to get kline data for 24h high/low + RSI
|
|
50
54
|
let high24h = price;
|
|
51
55
|
let low24h = price;
|
|
@@ -113,10 +117,11 @@ export function evaluateCondition(condition, snapshot, context) {
|
|
|
113
117
|
return !isNaN(snapshot.rsi) && snapshot.rsi > val;
|
|
114
118
|
case "rsi_below":
|
|
115
119
|
return !isNaN(snapshot.rsi) && snapshot.rsi < val;
|
|
120
|
+
// Unknown equity (null) never meets an equity condition — neither a fake stop nor a fake entry.
|
|
116
121
|
case "balance_above":
|
|
117
|
-
return context.equity > val;
|
|
122
|
+
return context.equity !== null && context.equity > val;
|
|
118
123
|
case "balance_below":
|
|
119
|
-
return context.equity < val;
|
|
124
|
+
return context.equity !== null && context.equity < val;
|
|
120
125
|
case "spread_above":
|
|
121
126
|
// Bid-ask spread percentage, pre-computed from orderbook in snapshot.
|
|
122
127
|
return snapshot.spreadPct > val;
|
|
@@ -125,7 +130,7 @@ export function evaluateCondition(condition, snapshot, context) {
|
|
|
125
130
|
return elapsed > val;
|
|
126
131
|
}
|
|
127
132
|
case "max_drawdown":
|
|
128
|
-
return (context.peakEquity - context.equity) > val;
|
|
133
|
+
return context.equity !== null && context.peakEquity !== null && (context.peakEquity - context.equity) > val;
|
|
129
134
|
default:
|
|
130
135
|
return false;
|
|
131
136
|
}
|
package/dist/bot/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VenueId } from "../exchanges/venue.js";
|
|
1
2
|
export type ConditionType = "price_above" | "price_below" | "volatility_above" | "volatility_below" | "funding_rate_above" | "funding_rate_below" | "spread_above" | "rsi_above" | "rsi_below" | "balance_above" | "balance_below" | "time_after" | "max_drawdown" | "always";
|
|
2
3
|
export interface Condition {
|
|
3
4
|
type: ConditionType;
|
|
@@ -59,7 +60,22 @@ export interface BotConfig {
|
|
|
59
60
|
risk: RiskConfig;
|
|
60
61
|
monitor_interval_sec: number;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The venue selected on the command line: the root `-e` value (resolved id) and whether it was
|
|
65
|
+
* given explicitly (commander option source "cli") or is the CLI default.
|
|
66
|
+
*/
|
|
67
|
+
export interface CliVenueSelection {
|
|
68
|
+
id: VenueId;
|
|
69
|
+
explicit: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The venue a config runs on. `config.exchange` (alias resolved; unknown → UNKNOWN_VENUE) wins
|
|
73
|
+
* over the CLI default; an explicit root `-e` naming a different venue is a contradiction →
|
|
74
|
+
* INVALID_PARAMS (never one of them silently). No `exchange` in the config → the CLI selection.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveConfigVenue(configExchange: string | null, cli: CliVenueSelection): VenueId;
|
|
77
|
+
/** Load a bot config; its venue is resolved against the CLI selection (resolveConfigVenue). */
|
|
78
|
+
export declare function loadBotConfig(pathOrInline: string, cli: CliVenueSelection): BotConfig;
|
|
63
79
|
export declare function parseStrategy(type: string, raw: Record<string, unknown>): StrategyParams;
|
|
64
80
|
export declare function quickGridConfig(opts: {
|
|
65
81
|
exchange: string;
|
package/dist/bot/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
|
+
import { PerpError, extractErrorMessage } from "../errors.js";
|
|
4
|
+
import { resolveVenueId } from "../exchanges/venue.js";
|
|
3
5
|
// ── Defaults ──
|
|
4
6
|
const DEFAULT_RISK = {
|
|
5
7
|
max_position_usd: 1000,
|
|
@@ -8,27 +10,61 @@ const DEFAULT_RISK = {
|
|
|
8
10
|
pause_after_loss_sec: 300,
|
|
9
11
|
max_open_bots: 5,
|
|
10
12
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The venue a config runs on. `config.exchange` (alias resolved; unknown → UNKNOWN_VENUE) wins
|
|
15
|
+
* over the CLI default; an explicit root `-e` naming a different venue is a contradiction →
|
|
16
|
+
* INVALID_PARAMS (never one of them silently). No `exchange` in the config → the CLI selection.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveConfigVenue(configExchange, cli) {
|
|
19
|
+
if (configExchange === null)
|
|
20
|
+
return cli.id;
|
|
21
|
+
const id = resolveVenueId(configExchange);
|
|
22
|
+
if (cli.explicit && cli.id !== id) {
|
|
23
|
+
throw new PerpError("INVALID_PARAMS", `The config runs on "${configExchange}" (${id}) but -e selects ${cli.id}`, {
|
|
24
|
+
remediation: `Drop -e, or pass -e ${id}, or change "exchange" in the config`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
/** Load a bot config; its venue is resolved against the CLI selection (resolveConfigVenue). */
|
|
30
|
+
export function loadBotConfig(pathOrInline, cli) {
|
|
13
31
|
let raw;
|
|
32
|
+
const remediation = "Pass a readable YAML/JSON config file path (or inline JSON). See examples with: perp strategy example";
|
|
14
33
|
if (existsSync(pathOrInline)) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
let content;
|
|
35
|
+
try {
|
|
36
|
+
content = readFileSync(pathOrInline, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new PerpError("INVALID_PARAMS", `Cannot read bot config file '${pathOrInline}': ${extractErrorMessage(err)}`, { remediation });
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
raw = pathOrInline.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
18
43
|
}
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new PerpError("INVALID_PARAMS", `Bot config file '${pathOrInline}' is not valid ${pathOrInline.endsWith(".json") ? "JSON" : "YAML"}: ${extractErrorMessage(err)}`, { remediation });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (pathOrInline.trimStart().startsWith("{")) {
|
|
49
|
+
// Inline JSON config
|
|
50
|
+
try {
|
|
51
|
+
raw = JSON.parse(pathOrInline);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new PerpError("INVALID_PARAMS", `Inline bot config is not valid JSON: ${extractErrorMessage(err)}`, { remediation });
|
|
21
55
|
}
|
|
22
56
|
}
|
|
23
57
|
else {
|
|
24
|
-
|
|
25
|
-
raw = JSON.parse(pathOrInline);
|
|
58
|
+
throw new PerpError("INVALID_PARAMS", `Bot config file not found: '${pathOrInline}'`, { remediation });
|
|
26
59
|
}
|
|
27
|
-
return parseBotConfig(raw);
|
|
60
|
+
return parseBotConfig(raw, cli);
|
|
28
61
|
}
|
|
29
|
-
function parseBotConfig(raw) {
|
|
62
|
+
function parseBotConfig(raw, cli) {
|
|
30
63
|
const name = String(raw.name ?? `bot-${Date.now()}`);
|
|
31
|
-
|
|
64
|
+
if (raw.exchange !== undefined && (typeof raw.exchange !== "string" || raw.exchange.trim() === "")) {
|
|
65
|
+
throw new PerpError("INVALID_PARAMS", `Bot config "exchange" must be a venue name (got ${JSON.stringify(raw.exchange)})`);
|
|
66
|
+
}
|
|
67
|
+
const exchange = resolveConfigVenue(typeof raw.exchange === "string" ? raw.exchange : null, cli);
|
|
32
68
|
const symbol = String(raw.symbol ?? "ETH");
|
|
33
69
|
const monitorInterval = Number(raw.monitor_interval_sec ?? raw.interval ?? 30);
|
|
34
70
|
// Strategy
|