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
|
@@ -37,3 +37,25 @@ export interface TradeCheckParams {
|
|
|
37
37
|
* Runs multiple checks in parallel where possible.
|
|
38
38
|
*/
|
|
39
39
|
export declare function validateTrade(adapter: ExchangeAdapter, params: TradeCheckParams): Promise<TradeValidation>;
|
|
40
|
+
/**
|
|
41
|
+
* Hard pre-trade risk gate for manual order commands.
|
|
42
|
+
*
|
|
43
|
+
* Unlike validateTrade (which is advisory — used by `trade check` and the MCP
|
|
44
|
+
* preview), this is meant to run immediately before submitting a real order.
|
|
45
|
+
* It throws RISK_VIOLATION when the order would breach the locally configured
|
|
46
|
+
* limits (maxPositionUsd / maxTotalExposureUsd / maxPositions, plus any active
|
|
47
|
+
* critical violation that suspends trading). reduce-only and --force orders are
|
|
48
|
+
* exempt, so position-reducing and intentional-override flows are never blocked.
|
|
49
|
+
*
|
|
50
|
+
* Leverage is only checked when the caller passes an explicit value; the bare
|
|
51
|
+
* buy/sell/market/limit commands carry no leverage argument, so the gate there
|
|
52
|
+
* enforces notional/exposure rather than per-order leverage.
|
|
53
|
+
*/
|
|
54
|
+
export declare function enforceOrderRisk(adapter: ExchangeAdapter, p: {
|
|
55
|
+
symbol: string;
|
|
56
|
+
size: number;
|
|
57
|
+
price?: number;
|
|
58
|
+
leverage?: number;
|
|
59
|
+
reduceOnly?: boolean;
|
|
60
|
+
force?: boolean;
|
|
61
|
+
}): Promise<void>;
|
package/dist/trade-validator.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { maxLeverageForNotional } from "./exchanges/interface.js";
|
|
1
2
|
import { symbolMatch } from "./utils.js";
|
|
2
3
|
import { DEFAULT_TAKER_FEE } from "./constants.js";
|
|
4
|
+
import { PerpError } from "./errors.js";
|
|
5
|
+
import { resolveAdapterSlippage } from "./exchanges/slippage.js";
|
|
3
6
|
// Use centralized DEFAULT_TAKER_FEE from constants.ts (0.035%)
|
|
4
|
-
const DEFAULT_MAX_SLIPPAGE = 0.005; // 0.5%
|
|
5
7
|
/**
|
|
6
8
|
* Validate a trade before execution.
|
|
7
9
|
* Runs multiple checks in parallel where possible.
|
|
@@ -10,6 +12,8 @@ export async function validateTrade(adapter, params) {
|
|
|
10
12
|
const checks = [];
|
|
11
13
|
const warnings = [];
|
|
12
14
|
const sym = params.symbol.toUpperCase();
|
|
15
|
+
// Liquidity threshold = the bound a market order on this venue would carry (design §13.10 #14).
|
|
16
|
+
const maxSlippage = resolveAdapterSlippage(adapter, null).bps / 10_000;
|
|
13
17
|
// Fetch market list, balance, and positions in parallel.
|
|
14
18
|
// Defer the orderbook call until symbol membership is confirmed so invalid
|
|
15
19
|
// symbols return a structured symbol_valid failure instead of a venue error.
|
|
@@ -29,11 +33,8 @@ export async function validateTrade(adapter, params) {
|
|
|
29
33
|
checks.push({ check: "symbol_valid", passed: false, message: `Failed to load markets from ${adapter.name}: ${marketsError}` });
|
|
30
34
|
return { valid: false, checks, warnings, timestamp: new Date().toISOString() };
|
|
31
35
|
}
|
|
32
|
-
// 1. Symbol validity (
|
|
33
|
-
const market = markets.find(m =>
|
|
34
|
-
const ms = m.symbol.toUpperCase();
|
|
35
|
-
return ms === sym || ms === `${sym}-PERP` || ms.replace(/-PERP$/, "") === sym;
|
|
36
|
-
});
|
|
36
|
+
// 1. Symbol validity (BTC vs BTC-PERP; a HIP-3 dex adapter lists "xyz:NVDA" for "NVDA")
|
|
37
|
+
const market = markets.find(m => symbolMatch(m.symbol, sym));
|
|
37
38
|
if (market) {
|
|
38
39
|
checks.push({ check: "symbol_valid", passed: true, message: `${sym} found on ${adapter.name}`, details: { maxLeverage: market.maxLeverage } });
|
|
39
40
|
}
|
|
@@ -45,6 +46,13 @@ export async function validateTrade(adapter, params) {
|
|
|
45
46
|
? await adapter.getOrderbook(sym)
|
|
46
47
|
: { bids: [], asks: [] };
|
|
47
48
|
const markPrice = Number(market.markPrice);
|
|
49
|
+
// Rule #2: NaN slipped past the previous `markPrice <= 0` check because
|
|
50
|
+
// NaN comparisons are always false. Reject explicit non-finite values
|
|
51
|
+
// upfront so downstream NaN propagation (notional, marginRequired,
|
|
52
|
+
// slippage, deviation) is impossible.
|
|
53
|
+
if (Number.isNaN(markPrice) || markPrice === Infinity || markPrice === -Infinity) {
|
|
54
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name} returned non-finite markPrice for ${sym}: ${market.markPrice}`, { exchange: adapter.name });
|
|
55
|
+
}
|
|
48
56
|
if (params.type === "limit" && (params.price === undefined || !Number.isFinite(params.price))) {
|
|
49
57
|
throw new Error(`Limit order requires explicit price (received ${params.price}); refusing silent mark-price substitution.`);
|
|
50
58
|
}
|
|
@@ -56,13 +64,25 @@ export async function validateTrade(adapter, params) {
|
|
|
56
64
|
if (params.leverage !== undefined && (!Number.isFinite(params.leverage) || params.leverage <= 0)) {
|
|
57
65
|
throw new Error(`Leverage must be a positive number (received ${params.leverage}).`);
|
|
58
66
|
}
|
|
59
|
-
|
|
67
|
+
// `market.maxLeverage` is the TOP tier. Venues that publish notional-tiered
|
|
68
|
+
// caps (Hyperliquid: 36 of 233 symbols, e.g. BTC 40x but 20x above $150M)
|
|
69
|
+
// reject an order sized past a threshold even though the flat value allows it,
|
|
70
|
+
// so resolve the cap AT THIS ORDER'S NOTIONAL.
|
|
71
|
+
const effectiveMax = maxLeverageForNotional(market, notional);
|
|
72
|
+
const leverage = params.leverage ?? effectiveMax;
|
|
60
73
|
if (!Number.isFinite(leverage) || leverage <= 0) {
|
|
61
74
|
throw new Error(`No usable leverage for ${sym} (resolved=${leverage}); supply params.leverage explicitly.`);
|
|
62
75
|
}
|
|
63
76
|
const marginRequired = notional / leverage;
|
|
64
77
|
// 2. Balance check
|
|
65
78
|
const available = Number(balance.available);
|
|
79
|
+
// Rule #2: a NaN `available` would silently take the "insufficient
|
|
80
|
+
// balance" branch on the negative comparison and *also* render
|
|
81
|
+
// misleading "$NaN available" in the user-visible message. Throw
|
|
82
|
+
// instead so the trade flow surfaces a real venue/parsing failure.
|
|
83
|
+
if (!Number.isFinite(available)) {
|
|
84
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name} returned non-finite balance.available: ${balance.available}`, { exchange: adapter.name });
|
|
85
|
+
}
|
|
66
86
|
if (params.reduceOnly) {
|
|
67
87
|
// reduce-only doesn't need margin
|
|
68
88
|
checks.push({ check: "balance_sufficient", passed: true, message: "Reduce-only order, no margin needed" });
|
|
@@ -96,15 +116,25 @@ export async function validateTrade(adapter, params) {
|
|
|
96
116
|
let availableLiquidity = 0;
|
|
97
117
|
let worstPrice = 0;
|
|
98
118
|
for (const [px, sz] of book) {
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
const pxN = Number(px);
|
|
120
|
+
const szN = Number(sz);
|
|
121
|
+
// Rule #2: a NaN level would silently inflate `availableLiquidity`
|
|
122
|
+
// to NaN, the `>= notional` comparison would be false, and the
|
|
123
|
+
// outer branch would emit a "Insufficient liquidity: $NaN" message.
|
|
124
|
+
// Reject the level explicitly so a malformed orderbook can't
|
|
125
|
+
// fabricate a passing or failing liquidity check.
|
|
126
|
+
if (!Number.isFinite(pxN) || !Number.isFinite(szN) || pxN <= 0 || szN < 0) {
|
|
127
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name} orderbook level has non-finite or non-positive value for ${sym}: px=${px} sz=${sz}`, { exchange: adapter.name });
|
|
128
|
+
}
|
|
129
|
+
availableLiquidity += pxN * szN;
|
|
130
|
+
worstPrice = pxN;
|
|
101
131
|
if (availableLiquidity >= notional)
|
|
102
132
|
break;
|
|
103
133
|
}
|
|
104
134
|
if (availableLiquidity >= notional) {
|
|
105
135
|
const slippage = markPrice > 0 ? Math.abs(worstPrice - markPrice) / markPrice : 0;
|
|
106
|
-
if (slippage >
|
|
107
|
-
checks.push({ check: "liquidity_ok", passed: false, message: `Slippage ${(slippage * 100).toFixed(2)}% exceeds ${(
|
|
136
|
+
if (slippage > maxSlippage) {
|
|
137
|
+
checks.push({ check: "liquidity_ok", passed: false, message: `Slippage ${(slippage * 100).toFixed(2)}% exceeds the ${(maxSlippage * 100).toFixed(2)}% market-order slippage bound`, details: { slippagePct: slippage * 100, worstPrice, liquidityUsd: availableLiquidity } });
|
|
108
138
|
}
|
|
109
139
|
else {
|
|
110
140
|
checks.push({ check: "liquidity_ok", passed: true, message: `Sufficient liquidity ($${availableLiquidity.toFixed(0)}), slippage ~${(slippage * 100).toFixed(3)}%`, details: { slippagePct: slippage * 100, liquidityUsd: availableLiquidity } });
|
|
@@ -121,7 +151,11 @@ export async function validateTrade(adapter, params) {
|
|
|
121
151
|
try {
|
|
122
152
|
const { assessRisk, preTradeCheck } = await import("./risk.js");
|
|
123
153
|
const assessment = assessRisk([{ exchange: adapter.name, balance }], positions.map(p => ({ exchange: adapter.name, position: p })));
|
|
124
|
-
|
|
154
|
+
// The leverage limit is checked only for a leverage the caller STATED (--leverage), exactly
|
|
155
|
+
// like enforceOrderRisk. Without one, `leverage` above is the market MAXIMUM (used for the
|
|
156
|
+
// margin estimate only) — not what the account trades at: checking it failed every order on
|
|
157
|
+
// a 50x market against a 20x limit while the account was set to 5x.
|
|
158
|
+
const riskResult = preTradeCheck(assessment, notional, params.leverage ?? 0);
|
|
125
159
|
if (riskResult.allowed) {
|
|
126
160
|
checks.push({ check: "risk_limits", passed: true, message: "Within risk limits" });
|
|
127
161
|
}
|
|
@@ -143,6 +177,11 @@ export async function validateTrade(adapter, params) {
|
|
|
143
177
|
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
144
178
|
if (pos) {
|
|
145
179
|
const posSize = parseFloat(pos.size);
|
|
180
|
+
// Rule #2: NaN posSize would make `params.size > posSize` always
|
|
181
|
+
// false, silently passing the reduce-only size check.
|
|
182
|
+
if (!Number.isFinite(posSize)) {
|
|
183
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name} returned non-finite position size for ${sym}: ${pos.size}`, { exchange: adapter.name });
|
|
184
|
+
}
|
|
146
185
|
if (params.size > posSize) {
|
|
147
186
|
checks.push({ check: "position_exists", passed: false, message: `Reduce size ${params.size} exceeds position ${posSize}`, details: { positionSize: posSize, reduceSize: params.size } });
|
|
148
187
|
}
|
|
@@ -158,8 +197,13 @@ export async function validateTrade(adapter, params) {
|
|
|
158
197
|
const fee = notional * DEFAULT_TAKER_FEE;
|
|
159
198
|
const estimatedSlippage = params.type === "limit" ? 0 : notional * 0.001;
|
|
160
199
|
const valid = checks.every(c => c.passed);
|
|
161
|
-
if (leverage >
|
|
162
|
-
|
|
200
|
+
if (leverage > effectiveMax) {
|
|
201
|
+
// Compare against the tier that applies at this notional, not the headline
|
|
202
|
+
// cap — otherwise the check is tautological whenever leverage defaulted FROM
|
|
203
|
+
// maxLeverage, and it never fires for the tiered case it exists to catch.
|
|
204
|
+
warnings.push(effectiveMax < market.maxLeverage
|
|
205
|
+
? `Requested leverage ${leverage}x exceeds ${effectiveMax}x, the cap for this $${notional.toFixed(0)} notional (headline max ${market.maxLeverage}x applies below the tier threshold)`
|
|
206
|
+
: `Requested leverage ${leverage}x exceeds max ${market.maxLeverage}x`);
|
|
163
207
|
}
|
|
164
208
|
return {
|
|
165
209
|
valid,
|
|
@@ -174,9 +218,69 @@ export async function validateTrade(adapter, params) {
|
|
|
174
218
|
marketInfo: {
|
|
175
219
|
symbol: sym,
|
|
176
220
|
markPrice,
|
|
177
|
-
|
|
221
|
+
// Rule #2: NaN funding rate must not propagate to envelope output;
|
|
222
|
+
// surface the missing-data state via warnings rather than emitting
|
|
223
|
+
// `fundingRate: NaN` to agents that JSON.parse the response.
|
|
224
|
+
fundingRate: (() => {
|
|
225
|
+
const fr = Number(market.fundingRate);
|
|
226
|
+
if (!Number.isFinite(fr)) {
|
|
227
|
+
warnings.push(`Funding rate unavailable for ${sym} on ${adapter.name} (received ${market.fundingRate})`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
return fr;
|
|
231
|
+
})(),
|
|
178
232
|
maxLeverage: market.maxLeverage,
|
|
179
233
|
},
|
|
180
234
|
timestamp: new Date().toISOString(),
|
|
181
235
|
};
|
|
182
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Hard pre-trade risk gate for manual order commands.
|
|
239
|
+
*
|
|
240
|
+
* Unlike validateTrade (which is advisory — used by `trade check` and the MCP
|
|
241
|
+
* preview), this is meant to run immediately before submitting a real order.
|
|
242
|
+
* It throws RISK_VIOLATION when the order would breach the locally configured
|
|
243
|
+
* limits (maxPositionUsd / maxTotalExposureUsd / maxPositions, plus any active
|
|
244
|
+
* critical violation that suspends trading). reduce-only and --force orders are
|
|
245
|
+
* exempt, so position-reducing and intentional-override flows are never blocked.
|
|
246
|
+
*
|
|
247
|
+
* Leverage is only checked when the caller passes an explicit value; the bare
|
|
248
|
+
* buy/sell/market/limit commands carry no leverage argument, so the gate there
|
|
249
|
+
* enforces notional/exposure rather than per-order leverage.
|
|
250
|
+
*/
|
|
251
|
+
export async function enforceOrderRisk(adapter, p) {
|
|
252
|
+
if (p.reduceOnly || p.force)
|
|
253
|
+
return;
|
|
254
|
+
const { loadRiskLimits, assessRisk, preTradeCheck } = await import("./risk.js");
|
|
255
|
+
const limits = loadRiskLimits();
|
|
256
|
+
// Resolve notional: limit orders carry a price; market orders need markPrice.
|
|
257
|
+
let price = p.price;
|
|
258
|
+
if (price == null || !Number.isFinite(price)) {
|
|
259
|
+
const markets = await adapter.getMarkets();
|
|
260
|
+
const m = markets.find((mk) => symbolMatch(mk.symbol, p.symbol));
|
|
261
|
+
if (!m) {
|
|
262
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `${p.symbol} not found on ${adapter.name}`, { exchange: adapter.name });
|
|
263
|
+
}
|
|
264
|
+
price = Number(m.markPrice);
|
|
265
|
+
}
|
|
266
|
+
// SSOT rule #2: if the notional cannot be computed we cannot enforce the
|
|
267
|
+
// limit — fail closed (the caller may pass --force) rather than wave it
|
|
268
|
+
// through with a silent zero notional that bypasses the position cap.
|
|
269
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
270
|
+
throw new PerpError("PRICE_STALE", `Cannot resolve a mark price for ${p.symbol} on ${adapter.name} to check risk limits`, {
|
|
271
|
+
exchange: adapter.name,
|
|
272
|
+
remediation: "Retry, or pass --force to skip the local risk check.",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const notional = Math.abs(p.size * price);
|
|
276
|
+
const [balance, positions] = await Promise.all([adapter.getBalance(), adapter.getPositions()]);
|
|
277
|
+
const assessment = assessRisk([{ exchange: adapter.name, balance }], positions.map((pos) => ({ exchange: adapter.name, position: pos })), limits);
|
|
278
|
+
const check = preTradeCheck(assessment, notional, p.leverage ?? 0);
|
|
279
|
+
if (!check.allowed) {
|
|
280
|
+
throw new PerpError("RISK_VIOLATION", check.reason ?? "Order blocked by local risk limits", {
|
|
281
|
+
exchange: adapter.name,
|
|
282
|
+
remediation: "Adjust limits with `perp risk set ...`, reduce the order size, or pass --force to override.",
|
|
283
|
+
details: { symbol: p.symbol, notional },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce a venue payload value to a finite number — or throw.
|
|
3
|
+
*
|
|
4
|
+
* Shared across all 4 exchange adapters (Lighter / Hyperliquid / Aster /
|
|
5
|
+
* Pacifica) to enforce Rule #2 ("no fallback") on venue-returned strings
|
|
6
|
+
* and numbers. Previously each adapter inlined `Number(x ?? 0)` which
|
|
7
|
+
* silently coerced NaN / Infinity / non-numeric strings to 0, masking
|
|
8
|
+
* stale-cache / partial-response corruption as legitimate "$0 balance".
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - `undefined` / `null` → returns `defaultValue` (default 0). Venues
|
|
12
|
+
* legitimately omit a field for an empty account or zero position;
|
|
13
|
+
* that is "no data, treat as zero", not a parsing failure.
|
|
14
|
+
* - empty string `""` → throws. Strict policy (qa/2026-05-16): an
|
|
15
|
+
* empty string is indistinguishable from a stale-cache partial
|
|
16
|
+
* response, so `Number("") === 0` would land back in the same
|
|
17
|
+
* silent-substitution hole. A truly absent field must surface as
|
|
18
|
+
* `undefined`/`null` at the adapter layer, not `""`.
|
|
19
|
+
* - finite number / parseable numeric string → returned as-is.
|
|
20
|
+
* - NaN / ±Infinity / non-numeric string → throws `EXCHANGE_ERROR`
|
|
21
|
+
* tagged with `exchange` so triage can attribute the failure to the
|
|
22
|
+
* exact venue endpoint.
|
|
23
|
+
*
|
|
24
|
+
* @param value Raw payload value (untyped — venues lie about SDK types).
|
|
25
|
+
* @param fieldName Field path for the error message (e.g.
|
|
26
|
+
* `"available_balance"`, `"position.unrealized_pnl"`).
|
|
27
|
+
* @param exchange Exchange tag for the PerpError details payload
|
|
28
|
+
* (`hyperliquid` / `pacifica` / `aster` / `lighter`).
|
|
29
|
+
* @param opts.defaultValue Value returned for null/undefined. Default `0`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseFiniteVenueNumber(value: unknown, fieldName: string, exchange: string, opts?: {
|
|
32
|
+
defaultValue?: number;
|
|
33
|
+
}): number;
|
|
34
|
+
/**
|
|
35
|
+
* A rate the venue sends as a JSON NUMBER, as a plain decimal string without float noise:
|
|
36
|
+
* rounded to 15 significant digits (a double's exact precision) and written without exponent.
|
|
37
|
+
* Lighter /funding-rates sends e.g. `"rate":0.00009599999999999999` → "0.000096";
|
|
38
|
+
* -0.000047999999999999994 → "-0.000048"; 1.2e-7 → "0.00000012". A rate the venue sends as a
|
|
39
|
+
* STRING is passed through verbatim by callers, never through this. Non-finite → EXCHANGE_ERROR.
|
|
40
|
+
*/
|
|
41
|
+
export declare function venueRateString(value: number, what?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* A required venue rate field as a decimal string: a JSON number → venueRateString (no float
|
|
44
|
+
* noise); a numeric string → verbatim. Missing / non-numeric → EXCHANGE_ERROR (never "0").
|
|
45
|
+
*/
|
|
46
|
+
export declare function venueRateField(value: unknown, fieldName: string, exchange: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { PerpError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Coerce a venue payload value to a finite number — or throw.
|
|
4
|
+
*
|
|
5
|
+
* Shared across all 4 exchange adapters (Lighter / Hyperliquid / Aster /
|
|
6
|
+
* Pacifica) to enforce Rule #2 ("no fallback") on venue-returned strings
|
|
7
|
+
* and numbers. Previously each adapter inlined `Number(x ?? 0)` which
|
|
8
|
+
* silently coerced NaN / Infinity / non-numeric strings to 0, masking
|
|
9
|
+
* stale-cache / partial-response corruption as legitimate "$0 balance".
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* - `undefined` / `null` → returns `defaultValue` (default 0). Venues
|
|
13
|
+
* legitimately omit a field for an empty account or zero position;
|
|
14
|
+
* that is "no data, treat as zero", not a parsing failure.
|
|
15
|
+
* - empty string `""` → throws. Strict policy (qa/2026-05-16): an
|
|
16
|
+
* empty string is indistinguishable from a stale-cache partial
|
|
17
|
+
* response, so `Number("") === 0` would land back in the same
|
|
18
|
+
* silent-substitution hole. A truly absent field must surface as
|
|
19
|
+
* `undefined`/`null` at the adapter layer, not `""`.
|
|
20
|
+
* - finite number / parseable numeric string → returned as-is.
|
|
21
|
+
* - NaN / ±Infinity / non-numeric string → throws `EXCHANGE_ERROR`
|
|
22
|
+
* tagged with `exchange` so triage can attribute the failure to the
|
|
23
|
+
* exact venue endpoint.
|
|
24
|
+
*
|
|
25
|
+
* @param value Raw payload value (untyped — venues lie about SDK types).
|
|
26
|
+
* @param fieldName Field path for the error message (e.g.
|
|
27
|
+
* `"available_balance"`, `"position.unrealized_pnl"`).
|
|
28
|
+
* @param exchange Exchange tag for the PerpError details payload
|
|
29
|
+
* (`hyperliquid` / `pacifica` / `aster` / `lighter`).
|
|
30
|
+
* @param opts.defaultValue Value returned for null/undefined. Default `0`.
|
|
31
|
+
*/
|
|
32
|
+
export function parseFiniteVenueNumber(value, fieldName, exchange, opts = {}) {
|
|
33
|
+
const defaultValue = opts.defaultValue ?? 0;
|
|
34
|
+
if (value === undefined || value === null)
|
|
35
|
+
return defaultValue;
|
|
36
|
+
if (value === "") {
|
|
37
|
+
throw new PerpError("EXCHANGE_ERROR", `${exchange} response field \`${fieldName}\` is an empty string (use null for missing data)`, { exchange });
|
|
38
|
+
}
|
|
39
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
40
|
+
if (!Number.isFinite(n)) {
|
|
41
|
+
throw new PerpError("EXCHANGE_ERROR", `${exchange} response field \`${fieldName}\` is not a finite number: ${JSON.stringify(value)}`, { exchange });
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
/** Significant digits kept when a venue rate arrives as a JSON number (IEEE-754 double: 15 are exact). */
|
|
46
|
+
const RATE_SIG_DIGITS = 15;
|
|
47
|
+
/**
|
|
48
|
+
* A rate the venue sends as a JSON NUMBER, as a plain decimal string without float noise:
|
|
49
|
+
* rounded to 15 significant digits (a double's exact precision) and written without exponent.
|
|
50
|
+
* Lighter /funding-rates sends e.g. `"rate":0.00009599999999999999` → "0.000096";
|
|
51
|
+
* -0.000047999999999999994 → "-0.000048"; 1.2e-7 → "0.00000012". A rate the venue sends as a
|
|
52
|
+
* STRING is passed through verbatim by callers, never through this. Non-finite → EXCHANGE_ERROR.
|
|
53
|
+
*/
|
|
54
|
+
export function venueRateString(value, what = "rate") {
|
|
55
|
+
if (!Number.isFinite(value))
|
|
56
|
+
throw new PerpError("EXCHANGE_ERROR", `${what} is not a finite number (${value})`);
|
|
57
|
+
if (value === 0)
|
|
58
|
+
return "0";
|
|
59
|
+
const [mant, expS] = value.toExponential(RATE_SIG_DIGITS - 1).split("e");
|
|
60
|
+
const digits = mant.replace("-", "").replace(".", "").replace(/0+$/, "");
|
|
61
|
+
const point = Number(expS) + 1; // value = 0.<digits> × 10^point
|
|
62
|
+
const sign = value < 0 ? "-" : "";
|
|
63
|
+
if (point <= 0)
|
|
64
|
+
return `${sign}0.${"0".repeat(-point)}${digits}`;
|
|
65
|
+
if (point >= digits.length)
|
|
66
|
+
return `${sign}${digits}${"0".repeat(point - digits.length)}`;
|
|
67
|
+
return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A required venue rate field as a decimal string: a JSON number → venueRateString (no float
|
|
71
|
+
* noise); a numeric string → verbatim. Missing / non-numeric → EXCHANGE_ERROR (never "0").
|
|
72
|
+
*/
|
|
73
|
+
export function venueRateField(value, fieldName, exchange) {
|
|
74
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
75
|
+
return venueRateString(value, `${exchange} ${fieldName}`);
|
|
76
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)))
|
|
77
|
+
return value;
|
|
78
|
+
throw new PerpError("EXCHANGE_ERROR", `${exchange} ${fieldName}: not a numeric rate (${JSON.stringify(value)})`, { exchange });
|
|
79
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OrderResult } from "./exchanges/venue-types.js";
|
|
1
2
|
export declare function symbolMatch(candidate: string, target: string): boolean;
|
|
2
3
|
export declare function formatUsd(value: string | number): string;
|
|
3
4
|
export declare function formatPnl(value: string | number): string;
|
|
@@ -46,6 +47,24 @@ export declare function jsonError(code: string, message: string, meta?: Partial<
|
|
|
46
47
|
* receive actionable hints. Codex v0.12.12 final QA #2.
|
|
47
48
|
*/
|
|
48
49
|
export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>, exchange?: string): Promise<T | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Central CLI error printer. JSON: the structured envelope incl. details and
|
|
52
|
+
* remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
|
|
53
|
+
* (design §13.6) — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
|
|
54
|
+
* otherwise `Error: <message>` plus the remediation.
|
|
55
|
+
*/
|
|
56
|
+
export declare function printCliError(err: unknown, isJson: boolean, exchange?: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* JSON payload of an order result (design §13.7): the uniform OrderResult (+ execution)
|
|
59
|
+
* with the venue's raw response duplicated under `legacy` for one release.
|
|
60
|
+
*/
|
|
61
|
+
export declare function orderResultJson(r: OrderResult & {
|
|
62
|
+
remediation?: string | null;
|
|
63
|
+
}, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
64
|
+
/** Human lines for an order result — one format for every venue (design §13.7). */
|
|
65
|
+
export declare function formatOrderResult(r: OrderResult & {
|
|
66
|
+
remediation?: string | null;
|
|
67
|
+
}): string[];
|
|
49
68
|
/**
|
|
50
69
|
* Log each rejected outcome from Promise.allSettled to stderr with a
|
|
51
70
|
* per-source label. SSOT rule #2 helper: turn a quiet rejection inside a
|
package/dist/utils.js
CHANGED
|
@@ -2,9 +2,19 @@ import Table from "cli-table3";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { classifyError } from "./errors.js";
|
|
4
4
|
export function symbolMatch(candidate, target) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Symmetric match: strip a trailing -PERP from BOTH sides before comparing.
|
|
6
|
+
// Callers pass (venueSymbol, userInput); the venue may report "BTC" or
|
|
7
|
+
// "BTC-PERP" and the user may type either form, so matching must not depend
|
|
8
|
+
// on which side carries the suffix.
|
|
9
|
+
const strip = (s) => s.toUpperCase().replace(/-PERP$/, "");
|
|
10
|
+
const c = strip(candidate);
|
|
11
|
+
const t = strip(target);
|
|
12
|
+
if (c === t)
|
|
13
|
+
return true;
|
|
14
|
+
// A HIP-3 dex-scoped adapter reports "xyz:NVDA" while the user (with --dex xyz) types "NVDA".
|
|
15
|
+
// One-way only: a dex-prefixed user input never matches a bare venue symbol.
|
|
16
|
+
const colon = c.indexOf(":");
|
|
17
|
+
return colon > 0 && !t.includes(":") && c.slice(colon + 1) === t;
|
|
8
18
|
}
|
|
9
19
|
export function formatUsd(value) {
|
|
10
20
|
const num = Number(value);
|
|
@@ -136,6 +146,10 @@ function downsample(values, targetLen) {
|
|
|
136
146
|
// ── JSON Output ──
|
|
137
147
|
const _isNdjson = () => process.argv.includes("--ndjson");
|
|
138
148
|
export function printJson(data) {
|
|
149
|
+
// An error envelope always means a non-zero exit (scripts / agents key on the exit code too):
|
|
150
|
+
// commands that print `jsonError(...)` and return would otherwise exit 0 with ok:false.
|
|
151
|
+
if (data !== null && typeof data === "object" && data.ok === false)
|
|
152
|
+
process.exitCode = 1;
|
|
139
153
|
// Sanitize all string values in output
|
|
140
154
|
let output = sanitizeValue(data);
|
|
141
155
|
// Apply --fields filter
|
|
@@ -213,25 +227,88 @@ export async function withJsonErrors(isJson, fn, exchange) {
|
|
|
213
227
|
return await fn();
|
|
214
228
|
}
|
|
215
229
|
catch (err) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(JSON.stringify(jsonError(classified.code, classified.message, {
|
|
219
|
-
status: classified.status,
|
|
220
|
-
retryable: classified.retryable,
|
|
221
|
-
retryAfterMs: classified.retryAfterMs,
|
|
222
|
-
remediation: classified.remediation,
|
|
223
|
-
details: classified.details,
|
|
224
|
-
})));
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
console.error(chalk.red(`Error: ${classified.message}`));
|
|
228
|
-
if (classified.remediation) {
|
|
229
|
-
console.error(chalk.yellow(`Remediation: ${classified.remediation}`));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
230
|
+
printCliError(err, isJson, exchange);
|
|
231
|
+
process.exitCode = 1; // printed, not swallowed: the command still exits non-zero
|
|
232
232
|
return undefined;
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Central CLI error printer. JSON: the structured envelope incl. details and
|
|
237
|
+
* remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
|
|
238
|
+
* (design §13.6) — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
|
|
239
|
+
* otherwise `Error: <message>` plus the remediation.
|
|
240
|
+
*/
|
|
241
|
+
export function printCliError(err, isJson, exchange) {
|
|
242
|
+
const classified = classifyError(err, exchange);
|
|
243
|
+
if (isJson) {
|
|
244
|
+
console.log(JSON.stringify(jsonError(classified.code, classified.message, {
|
|
245
|
+
status: classified.status,
|
|
246
|
+
retryable: classified.retryable,
|
|
247
|
+
retryAfterMs: classified.retryAfterMs,
|
|
248
|
+
remediation: classified.remediation,
|
|
249
|
+
details: classified.details,
|
|
250
|
+
})));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const d = classified.details;
|
|
254
|
+
if (classified.code === "CAPABILITY_UNSUPPORTED" && d && typeof d.venue === "string" && typeof d.feature === "string" && typeof d.reason === "string") {
|
|
255
|
+
const alt = classified.remediation !== undefined ? ` Alternative: ${classified.remediation}` : "";
|
|
256
|
+
console.error(chalk.red(`✗ ${d.venue}: ${d.feature} not supported — ${d.reason}.`) + chalk.yellow(alt));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.error(chalk.red(`Error: ${classified.message}`));
|
|
260
|
+
if (classified.remediation) {
|
|
261
|
+
console.error(chalk.yellow(`Remediation: ${classified.remediation}`));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* JSON payload of an order result (design §13.7): the uniform OrderResult (+ execution)
|
|
266
|
+
* with the venue's raw response duplicated under `legacy` for one release.
|
|
267
|
+
*/
|
|
268
|
+
export function orderResultJson(r, extra) {
|
|
269
|
+
return { ...r, ...extra, legacy: r.venueRaw };
|
|
270
|
+
}
|
|
271
|
+
/** Human lines for an order result — one format for every venue (design §13.7). */
|
|
272
|
+
export function formatOrderResult(r) {
|
|
273
|
+
const lines = [];
|
|
274
|
+
const statusColor = r.status === "filled" ? chalk.green
|
|
275
|
+
: r.status === "rejected" || r.status === "cancelled" ? chalk.red
|
|
276
|
+
: r.status === "partiallyFilled" ? chalk.yellow : chalk.white;
|
|
277
|
+
const fill = `filled ${r.filledSize ?? "unknown"}${r.avgPrice !== null ? ` @ ${r.avgPrice}` : ""}`;
|
|
278
|
+
lines.push(` Status: ${statusColor(r.status)} (${r.finality === "final" ? "final" : chalk.yellow("pending")}) — ${fill}`);
|
|
279
|
+
const ids = [];
|
|
280
|
+
if (r.orderId !== null)
|
|
281
|
+
ids.push(`order ${r.orderId}`);
|
|
282
|
+
if (r.clientOrderId !== null)
|
|
283
|
+
ids.push(`client id ${r.clientOrderId}`);
|
|
284
|
+
if (ids.length > 0)
|
|
285
|
+
lines.push(` Order: ${ids.join(" · ")}`);
|
|
286
|
+
const e = r.execution;
|
|
287
|
+
const exec = [e.method];
|
|
288
|
+
if (e.slippageBps !== null)
|
|
289
|
+
exec.push(`slippage ${Math.round(e.slippageBps * 100) / 100} bps${e.slippageSource !== null ? ` (${e.slippageSource})` : ""}`);
|
|
290
|
+
if (e.protectionPrice !== null)
|
|
291
|
+
exec.push(`protection ${e.protectionPrice}${e.referencePrice !== null ? ` (ref ${e.referencePrice})` : ""}`);
|
|
292
|
+
else if (e.referencePrice !== null)
|
|
293
|
+
exec.push(`ref ${e.referencePrice}`);
|
|
294
|
+
lines.push(` Execution: ${exec.join(" · ")}`);
|
|
295
|
+
if (e.tifRequested !== null || e.tifSent !== null) {
|
|
296
|
+
const tif = e.tifRequested !== null && e.tifSent !== null && e.tifRequested !== e.tifSent
|
|
297
|
+
? `${e.tifRequested} → ${e.tifSent}` : (e.tifSent ?? e.tifRequested);
|
|
298
|
+
lines.push(` TIF: ${tif}${e.expiresAt !== null ? ` (expires ${new Date(e.expiresAt).toISOString()})` : ""}`);
|
|
299
|
+
}
|
|
300
|
+
if (e.builderFee !== null) {
|
|
301
|
+
const approved = e.builderFee.approvedMaxBps === null ? "" : `, approved max ${e.builderFee.approvedMaxBps} bps`;
|
|
302
|
+
const fee = e.builderFee.feeKind === "share" ? "fee share (you pay the normal fee, nothing extra)"
|
|
303
|
+
: e.builderFee.feeKind === "max" ? `up to ${e.builderFee.feeBps} bps (builder's own rate applies${approved})` : `${e.builderFee.feeBps} bps`;
|
|
304
|
+
lines.push(` Builder: ${fee} → ${e.builderFee.identity}`);
|
|
305
|
+
}
|
|
306
|
+
for (const n of e.notes)
|
|
307
|
+
lines.push(chalk.gray(` Note: ${n}`));
|
|
308
|
+
if (r.remediation !== undefined && r.remediation !== null)
|
|
309
|
+
lines.push(chalk.yellow(` ${r.remediation}`));
|
|
310
|
+
return lines;
|
|
311
|
+
}
|
|
235
312
|
/**
|
|
236
313
|
* Log each rejected outcome from Promise.allSettled to stderr with a
|
|
237
314
|
* per-source label. SSOT rule #2 helper: turn a quiet rejection inside a
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Withdraw gate for venues whose withdrawals are authorized by an API key alone — no owner
|
|
3
|
+
* signature (settings.withdraw.<venue>; today: qfex). Design §14.12 #8.
|
|
4
|
+
*
|
|
5
|
+
* Before such a withdrawal is sent it must pass, locally:
|
|
6
|
+
* 1. a gate is configured at all (absent → refused);
|
|
7
|
+
* 2. the destination is on the allowlist;
|
|
8
|
+
* 3. amount ≤ maxPerTx;
|
|
9
|
+
* 4. today's total (UTC day, from this machine's ledger) + amount ≤ maxPerDay.
|
|
10
|
+
* The adapter enforces it inside withdraw(), so no caller can skip it.
|
|
11
|
+
*
|
|
12
|
+
* What it is NOT: a defence against key theft. Someone holding the key file does not use this
|
|
13
|
+
* CLI. That defence is a trading key WITHOUT the withdraw permission (a separate key for
|
|
14
|
+
* funding). The gate limits mistakes and misdirected automation: its allowlist can only be
|
|
15
|
+
* changed with an interactive terminal confirmation (confirmWithdrawGateChange), never under
|
|
16
|
+
* --json, a pipe, the MCP server or a job.
|
|
17
|
+
*
|
|
18
|
+
* Ledger: ~/.perp/withdraw-ledger.json — amounts and addresses only (no secrets, Rule #3).
|
|
19
|
+
* An attempt is recorded BEFORE it is sent and removed only on a definite refusal, so an
|
|
20
|
+
* unknown outcome counts against the day.
|
|
21
|
+
*/
|
|
22
|
+
import type { WithdrawGate, WithdrawGateVenue } from "./settings.js";
|
|
23
|
+
export interface WithdrawLedgerEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
venue: string;
|
|
26
|
+
network: string;
|
|
27
|
+
/** USDC, as sent. */
|
|
28
|
+
amount: string;
|
|
29
|
+
to: string;
|
|
30
|
+
/** ISO timestamp of the attempt. */
|
|
31
|
+
at: string;
|
|
32
|
+
}
|
|
33
|
+
/** Sum of the amounts attempted for venue + network on the UTC day of `nowMs`. */
|
|
34
|
+
export declare function withdrawnOnDay(venue: string, network: string, nowMs: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Throws POLICY_DENIED unless the withdrawal passes `gate`. Nothing is sent before this returns.
|
|
37
|
+
* `gate` null = settings.withdraw.<venue> is not configured.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertWithdrawAllowed(venue: WithdrawGateVenue, network: string, amount: string, to: string, gate: WithdrawGate | null, nowMs: number): void;
|
|
40
|
+
/** Record an attempt (call BEFORE sending). Returns its id for forgetWithdrawAttempt. */
|
|
41
|
+
export declare function recordWithdrawAttempt(venue: string, network: string, amount: string, to: string, nowMs: number): string;
|
|
42
|
+
/** Remove an attempt the venue DEFINITELY refused (nothing left the account). */
|
|
43
|
+
export declare function forgetWithdrawAttempt(id: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Confirmation for creating or changing settings.withdraw.<venue>. Only an interactive
|
|
46
|
+
* terminal can confirm: --json, a non-TTY stdin/stdout (pipes, the MCP server, jobs) are
|
|
47
|
+
* refused, and there is no --yes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function confirmWithdrawGateChange(venue: string, gate: WithdrawGate, json: boolean): Promise<void>;
|