perp-cli 0.14.2 → 0.15.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 +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +168 -36
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ui.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +9 -4
- package/dist/exchanges/aster.js +125 -55
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacifica credit rate limit, read from the venue's own response headers.
|
|
3
|
+
*
|
|
4
|
+
* docs.pacifica.fi api-documentation/api/rate-limits (read 2026-10-19): credit-based, rolling
|
|
5
|
+
* 60 s window; 100 credits / 60 s per unidentified IP, 300 with a valid API Config Key (more for
|
|
6
|
+
* higher fee tiers); standard request 1, cancel 0.5, heavy GET 3-12 (1-4 with a key). Exhausted →
|
|
7
|
+
* HTTP 429. "All credit values are multiplied by 10 to support fractional costs":
|
|
8
|
+
*
|
|
9
|
+
* ratelimit: "credits";r=1200;t=32 r = remaining (×10), t = seconds until refresh
|
|
10
|
+
* ratelimit-policy: "credits";q=1250;w=60 q = quota per window (×10), w = window seconds
|
|
11
|
+
*
|
|
12
|
+
* Live mainnet GET /info/prices (2026-10-19, no key): `ratelimit: "credits";r=980;t=22`,
|
|
13
|
+
* `ratelimit-policy: "credits";q=1000;w=60`. Values below stay in those ×10 units.
|
|
14
|
+
*
|
|
15
|
+
* CloudFront caches some GETs (/info: `x-cache: Hit from cloudfront`, `age: 16`): a cache hit
|
|
16
|
+
* replays the headers of whoever filled the cache (live: q=3000, a keyed caller) — not this
|
|
17
|
+
* caller's quota, so a hit carries no information.
|
|
18
|
+
*
|
|
19
|
+
* Scope: one state per REST base URL (mainnet / testnet), credential tier (no key = IP quota;
|
|
20
|
+
* an API Config Key has its own) and request kind (signed POST vs unsigned GET). Process-local: other perp-cli processes (CLI + MCP + bots) on the
|
|
21
|
+
* same IP spend the same quota unseen, so a 429 can still happen — it is then surfaced as
|
|
22
|
+
* RATE_LIMITED, never retried here.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from "crypto";
|
|
25
|
+
import { PerpError } from "../errors.js";
|
|
26
|
+
/** `"credits";k=v;k=v` with non-negative integer values; null on any deviation. */
|
|
27
|
+
function parseCreditsItem(raw) {
|
|
28
|
+
if (raw === null)
|
|
29
|
+
return null;
|
|
30
|
+
const parts = raw.trim().split(";").map((p) => p.trim());
|
|
31
|
+
if (parts[0] !== '"credits"')
|
|
32
|
+
return null;
|
|
33
|
+
const out = new Map();
|
|
34
|
+
for (const p of parts.slice(1)) {
|
|
35
|
+
const m = /^([a-z][a-z0-9_-]*)=(\d{1,12})$/.exec(p);
|
|
36
|
+
if (m === null || out.has(m[1]))
|
|
37
|
+
return null;
|
|
38
|
+
out.set(m[1], Number(m[2]));
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Strict parse of the `ratelimit` / `ratelimit-policy` header values. `ratelimit` absent or
|
|
44
|
+
* malformed (no `"credits"` item, missing / non-integer / duplicate r or t) → null: no info, never
|
|
45
|
+
* a guessed value. A malformed policy only nulls quota / windowSec.
|
|
46
|
+
*/
|
|
47
|
+
export function parsePacificaRateLimit(ratelimit, policy) {
|
|
48
|
+
const rl = parseCreditsItem(ratelimit);
|
|
49
|
+
const r = rl?.get("r");
|
|
50
|
+
const t = rl?.get("t");
|
|
51
|
+
if (r === undefined || t === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
const pol = parseCreditsItem(policy);
|
|
54
|
+
const q = pol?.get("q");
|
|
55
|
+
const w = pol?.get("w");
|
|
56
|
+
const policyOk = q !== undefined && w !== undefined && w > 0;
|
|
57
|
+
return { remaining: r, resetSec: t, quota: policyOk ? q : null, windowSec: policyOk ? w : null };
|
|
58
|
+
}
|
|
59
|
+
/** `Retry-After` in delta-seconds → ms; absent / HTTP-date / malformed → null. */
|
|
60
|
+
export function parseRetryAfterMs(raw) {
|
|
61
|
+
if (raw === null || !/^\d{1,9}$/.test(raw.trim()))
|
|
62
|
+
return null;
|
|
63
|
+
return Number(raw.trim()) * 1000;
|
|
64
|
+
}
|
|
65
|
+
function header(res, name) {
|
|
66
|
+
// Every real fetch Response has headers; a value without them carries no rate-limit info.
|
|
67
|
+
const h = res?.headers;
|
|
68
|
+
return h !== undefined && typeof h.get === "function" ? h.get(name) : null;
|
|
69
|
+
}
|
|
70
|
+
/** A CloudFront cache hit replays someone else's headers (see module doc). */
|
|
71
|
+
function isCacheHit(res) {
|
|
72
|
+
const xc = header(res, "x-cache");
|
|
73
|
+
return (xc !== null && /^(Hit|RefreshHit)\b/i.test(xc.trim())) || header(res, "age") !== null;
|
|
74
|
+
}
|
|
75
|
+
/** Parsed rate-limit info of a response this caller's request produced; null = no info. */
|
|
76
|
+
export function pacificaRateLimitOf(res) {
|
|
77
|
+
if (isCacheHit(res))
|
|
78
|
+
return null;
|
|
79
|
+
return parsePacificaRateLimit(header(res, "ratelimit"), header(res, "ratelimit-policy"));
|
|
80
|
+
}
|
|
81
|
+
/** Read reserve (credits ×10): the largest documented heavy-GET cost (12 credits) stays unspent after a read. */
|
|
82
|
+
export const PACIFICA_READ_RESERVE = 120;
|
|
83
|
+
/** Cost (credits ×10) of a standard request / action (docs: 1 credit). */
|
|
84
|
+
export const PACIFICA_COST_STANDARD = 10;
|
|
85
|
+
/** Cost (credits ×10) of an order cancellation (docs: 0.5 credits). */
|
|
86
|
+
export const PACIFICA_COST_CANCEL = 5;
|
|
87
|
+
/** Slack after the venue's `t` (whole seconds) before the window is treated as refreshed. */
|
|
88
|
+
const RESET_MARGIN_MS = 250;
|
|
89
|
+
/** Documented window (docs: "60-second rolling window") — only an upper clamp for `t` / Retry-After. */
|
|
90
|
+
const DOC_WINDOW_SEC = 60;
|
|
91
|
+
/** A reset / Retry-After longer than the window + this is clamped (a bogus value must not stall). */
|
|
92
|
+
const RESET_CLAMP_MARGIN_MS = 1_000;
|
|
93
|
+
/** Longest pre-send wait before failing fast with RATE_LIMITED. */
|
|
94
|
+
export const PACIFICA_MAX_THROTTLE_WAIT_MS = 15_000;
|
|
95
|
+
/** Longest wait between signing and sending (signed bodies expire after expiry_window = 5 s). */
|
|
96
|
+
export const PACIFICA_SIGNED_MAX_WAIT_MS = 2_000;
|
|
97
|
+
/** Monotonic: elapsed time never follows a wall-clock step (NTP, VM resume). */
|
|
98
|
+
const realClock = {
|
|
99
|
+
now: () => performance.now(),
|
|
100
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
101
|
+
};
|
|
102
|
+
export const PACIFICA_READ_NEED = { cost: PACIFICA_COST_STANDARD, reserve: PACIFICA_READ_RESERVE, maxWaitMs: PACIFICA_MAX_THROTTLE_WAIT_MS };
|
|
103
|
+
function clampResetMs(ms, windowSec) {
|
|
104
|
+
const cap = (windowSec !== null ? windowSec : DOC_WINDOW_SEC) * 1000 + RESET_CLAMP_MARGIN_MS;
|
|
105
|
+
return Math.min(ms, cap);
|
|
106
|
+
}
|
|
107
|
+
export class PacificaRateLimiter {
|
|
108
|
+
label;
|
|
109
|
+
clock;
|
|
110
|
+
obs = null;
|
|
111
|
+
/** Credits ×10 reserved by requests sent and not yet settled — not reset by observe(). */
|
|
112
|
+
inFlight = 0;
|
|
113
|
+
lastRaw = Number.NEGATIVE_INFINITY;
|
|
114
|
+
skew = 0;
|
|
115
|
+
constructor(label, clock = realClock) {
|
|
116
|
+
this.label = label;
|
|
117
|
+
this.clock = clock;
|
|
118
|
+
}
|
|
119
|
+
/** clock.now() made monotonic: a backwards step is absorbed instead of restarting elapsed time. */
|
|
120
|
+
now() {
|
|
121
|
+
const raw = this.clock.now();
|
|
122
|
+
if (raw < this.lastRaw)
|
|
123
|
+
this.skew += this.lastRaw - raw;
|
|
124
|
+
this.lastRaw = raw;
|
|
125
|
+
return raw + this.skew;
|
|
126
|
+
}
|
|
127
|
+
/** ms until the observed window refreshes; null = no live observation. */
|
|
128
|
+
resetWaitMs() {
|
|
129
|
+
if (this.obs === null)
|
|
130
|
+
return null;
|
|
131
|
+
const left = this.obs.resetInMs + RESET_MARGIN_MS - (this.now() - this.obs.observedAt);
|
|
132
|
+
if (left <= 0) {
|
|
133
|
+
this.obs = null;
|
|
134
|
+
return null;
|
|
135
|
+
} // window refreshed: the observation is spent
|
|
136
|
+
return left;
|
|
137
|
+
}
|
|
138
|
+
/** Current state for diagnostics (credits ×10); null = no live observation. */
|
|
139
|
+
snapshot() {
|
|
140
|
+
const wait = this.resetWaitMs();
|
|
141
|
+
if (wait === null || this.obs === null)
|
|
142
|
+
return null;
|
|
143
|
+
return { remaining: this.obs.remaining, inFlight: this.inFlight, effectiveRemaining: this.obs.remaining - this.inFlight, resetInMs: wait };
|
|
144
|
+
}
|
|
145
|
+
/** ms to wait before a request with `need` may be sent: 0 = now (no info, or enough credits left). */
|
|
146
|
+
waitMs(need) {
|
|
147
|
+
const wait = this.resetWaitMs();
|
|
148
|
+
if (wait === null || this.obs === null)
|
|
149
|
+
return 0;
|
|
150
|
+
return this.obs.remaining - this.inFlight - need.cost >= need.reserve ? 0 : wait;
|
|
151
|
+
}
|
|
152
|
+
ticket(cost) {
|
|
153
|
+
this.inFlight += cost;
|
|
154
|
+
let open = true;
|
|
155
|
+
const release = () => { if (open) {
|
|
156
|
+
open = false;
|
|
157
|
+
this.inFlight -= cost;
|
|
158
|
+
} };
|
|
159
|
+
return {
|
|
160
|
+
release,
|
|
161
|
+
settle: (res) => { release(); return this.observe(res); },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Pre-send throttle + reservation. Enough credits (or no info) → the cost is reserved
|
|
166
|
+
* synchronously, before any await, so concurrent callers see each other. Otherwise waits for the
|
|
167
|
+
* window to refresh (total ≤ need.maxWaitMs) or fails fast with RATE_LIMITED (sent: false).
|
|
168
|
+
*/
|
|
169
|
+
async acquire(what, need) {
|
|
170
|
+
let budget = need.maxWaitMs;
|
|
171
|
+
for (;;) {
|
|
172
|
+
const wait = this.waitMs(need);
|
|
173
|
+
if (wait === 0)
|
|
174
|
+
return this.ticket(need.cost);
|
|
175
|
+
if (wait > budget)
|
|
176
|
+
throw this.throttled(what, wait);
|
|
177
|
+
const waitedOn = this.obs;
|
|
178
|
+
const start = this.now();
|
|
179
|
+
await this.clock.sleep(wait);
|
|
180
|
+
budget -= this.now() - start;
|
|
181
|
+
// The window that observation described has refreshed; a newer one (a response that landed
|
|
182
|
+
// meanwhile) is kept and re-checked.
|
|
183
|
+
if (this.obs === waitedOn)
|
|
184
|
+
this.obs = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Like acquire but never waits or refuses (health probe): reserves and records only. */
|
|
188
|
+
track(cost) {
|
|
189
|
+
return this.ticket(cost);
|
|
190
|
+
}
|
|
191
|
+
/** Wait (no reservation) until `need` would pass — used before signing, so the post-sign acquire finds credits. */
|
|
192
|
+
async awaitCapacity(what, need) {
|
|
193
|
+
(await this.acquire(what, need)).release();
|
|
194
|
+
}
|
|
195
|
+
throttled(what, wait) {
|
|
196
|
+
return new PerpError("RATE_LIMITED", `Pacifica ${what} not sent: rate-limit credits nearly exhausted (${this.label}), window refreshes in ${Math.ceil(wait / 1000)} s`, {
|
|
197
|
+
exchange: "pacifica", sent: false, retryAfterMs: Math.ceil(wait),
|
|
198
|
+
remediation: `Nothing was sent. Retry after ${Math.ceil(wait / 1000)} s; other perp-cli processes on this IP share the same Pacifica quota — reduce request frequency or use an API Config Key (300 credits / 60 s)`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Record a response's headers (any status). No / malformed / cached headers → unchanged. Within
|
|
203
|
+
* the live window an observation with a higher `r` (a late response from before others landed)
|
|
204
|
+
* or a different `q` (another quota: account vs IP) is dropped.
|
|
205
|
+
*/
|
|
206
|
+
observe(res) {
|
|
207
|
+
const info = pacificaRateLimitOf(res);
|
|
208
|
+
if (info === null)
|
|
209
|
+
return null;
|
|
210
|
+
const live = this.resetWaitMs() !== null && this.obs !== null ? this.obs : null;
|
|
211
|
+
if (live !== null) {
|
|
212
|
+
if (info.remaining > live.remaining)
|
|
213
|
+
return null;
|
|
214
|
+
if (live.quota !== null && info.quota !== null && info.quota !== live.quota)
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
this.obs = { remaining: info.remaining, resetInMs: clampResetMs(info.resetSec * 1000, info.windowSec), observedAt: this.now(), quota: info.quota };
|
|
218
|
+
return info;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* HTTP 429 → RATE_LIMITED (structural, by status). retryAfterMs = `Retry-After`, else the
|
|
222
|
+
* ratelimit reset `t`, else absent (clamped to the window). The quota is recorded as exhausted.
|
|
223
|
+
*
|
|
224
|
+
* `sent: false` ("not executed") is OUR reading of the docs — "When credits are exhausted,
|
|
225
|
+
* requests return HTTP 429" — i.e. the request is refused before processing. Pacifica does not
|
|
226
|
+
* state explicitly that a 429'd write was not executed.
|
|
227
|
+
*/
|
|
228
|
+
rateLimitedError(what, res, body) {
|
|
229
|
+
const info = pacificaRateLimitOf(res);
|
|
230
|
+
const fromHeader = parseRetryAfterMs(header(res, "retry-after"));
|
|
231
|
+
const raw = fromHeader !== null ? fromHeader : info !== null ? info.resetSec * 1000 : null;
|
|
232
|
+
const retryAfterMs = raw !== null ? clampResetMs(raw, info !== null ? info.windowSec : null) : null;
|
|
233
|
+
if (retryAfterMs !== null)
|
|
234
|
+
this.obs = { remaining: 0, resetInMs: retryAfterMs, observedAt: this.now(), quota: info !== null ? info.quota : null };
|
|
235
|
+
return new PerpError("RATE_LIMITED", `Pacifica ${what} rate limited (HTTP 429); nothing was executed: ${body.slice(0, 200)}`, {
|
|
236
|
+
exchange: "pacifica", httpStatus: 429, sent: false,
|
|
237
|
+
...(retryAfterMs !== null ? { retryAfterMs } : {}),
|
|
238
|
+
remediation: `${retryAfterMs !== null ? `Retry after ${Math.ceil(retryAfterMs / 1000)} s` : "Wait for the 60 s window to refresh, then retry"}; other perp-cli processes on this IP share the same Pacifica quota — reduce request frequency or use an API Config Key (300 credits / 60 s)`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** A Pacifica RATE_LIMITED that sent nothing (pre-send throttle or HTTP 429): a definite refusal. */
|
|
243
|
+
export function isPacificaRateLimited(e) {
|
|
244
|
+
return e instanceof PerpError && e.structured.code === "RATE_LIMITED"
|
|
245
|
+
&& e.structured.details?.exchange === "pacifica" && e.structured.details?.sent === false;
|
|
246
|
+
}
|
|
247
|
+
const limiters = new Map();
|
|
248
|
+
/**
|
|
249
|
+
* The process-local limiter for a REST base URL + credential tier (no key = IP quota) + request
|
|
250
|
+
* kind. Signed POSTs are kept apart from unsigned GETs: credits are shared per main account and
|
|
251
|
+
* scale with its fee tier, so signed responses can report an account quota (e.g. q=3000) while
|
|
252
|
+
* keyless GETs report the IP quota (q=1000).
|
|
253
|
+
*/
|
|
254
|
+
export function pacificaRateLimiter(baseUrl, apiKey, kind) {
|
|
255
|
+
const tier = apiKey ? `key:${createHash("sha256").update(apiKey).digest("hex").slice(0, 12)}` : "ip";
|
|
256
|
+
const key = `${baseUrl}|${tier}|${kind}`;
|
|
257
|
+
let l = limiters.get(key);
|
|
258
|
+
if (l === undefined) {
|
|
259
|
+
l = new PacificaRateLimiter(`${baseUrl} ${apiKey ? "API Config Key" : "IP"} ${kind} quota`);
|
|
260
|
+
limiters.set(key, l);
|
|
261
|
+
}
|
|
262
|
+
return l;
|
|
263
|
+
}
|
|
264
|
+
/** Test hook: forget every limiter's state. */
|
|
265
|
+
export function resetPacificaRateLimiters() {
|
|
266
|
+
limiters.clear();
|
|
267
|
+
}
|
|
@@ -71,7 +71,8 @@ export interface TPSLParams {
|
|
|
71
71
|
stop_price: string;
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
/** docs rest-api/orders/batch-order `type` (case sensitive; not the signing operation type). */
|
|
75
|
+
export type BatchActionType = "Create" | "CreateMarket" | "Cancel" | "Edit" | "SetPositionTpsl" | "CancelStopOrder";
|
|
75
76
|
export interface BatchAction {
|
|
76
77
|
type: BatchActionType;
|
|
77
78
|
data: Record<string, unknown>;
|
package/dist/plan-executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertCanClosePositions } from "./exchanges/trading-v2.js";
|
|
2
|
+
import { getVenue, requireRole, tryResolveVenueId } from "./exchanges/venue.js";
|
|
2
3
|
import { symbolMatch } from "./utils.js";
|
|
3
4
|
import { logExecution } from "./execution-log.js";
|
|
4
5
|
import { randomUUID } from "crypto";
|
|
@@ -83,6 +84,10 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
83
84
|
const completedSteps = new Map();
|
|
84
85
|
const dryRun = opts?.dryRun ?? false;
|
|
85
86
|
const log = opts?.log ?? (() => { });
|
|
87
|
+
// A venue without the trading role (TxFlow, release-gated RISEx) runs no plan — dry-run included.
|
|
88
|
+
const venueId = tryResolveVenueId(adapter.name);
|
|
89
|
+
if (venueId !== null)
|
|
90
|
+
requireRole(getVenue(venueId), "trading", "plan");
|
|
86
91
|
// A plan that places orders must be able to close them (rollback / close_position are
|
|
87
92
|
// reduce-only): refuse before the first step on a venue that cannot.
|
|
88
93
|
if (!dryRun && plan.steps.some(s => PLAN_ORDER_ACTIONS.has(s.action))) {
|
|
@@ -155,7 +160,7 @@ export async function executePlan(adapter, plan, opts) {
|
|
|
155
160
|
const onFailure = step.onFailure ?? "abort";
|
|
156
161
|
if (ORDER_ACTIONS.has(step.action) && isOutcomeUnknown(err)) {
|
|
157
162
|
// The order may have been placed: never treat it as failed, never resubmit or roll it
|
|
158
|
-
// back. Stop the plan and quarantine the symbol
|
|
163
|
+
// back. Stop the plan and quarantine the symbol.
|
|
159
164
|
const sym = String(step.params.symbol ?? "").toUpperCase();
|
|
160
165
|
log(`[plan:${planId}] Step ${step.id} (${step.action}): OUTCOME UNKNOWN — ${message}`);
|
|
161
166
|
quarantine(sym, `step ${step.id} (${step.action}) outcome unknown: ${message}`);
|
|
@@ -300,7 +305,7 @@ async function executeStep(adapter, step, log, placed) {
|
|
|
300
305
|
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
301
306
|
// Size from the live position (abs), reduce-only: a stale / repeated close can never open
|
|
302
307
|
// or flip a position — the venue rejects it instead. Fill measured (ORDER_PENDING when unconfirmed).
|
|
303
|
-
// A partial close (venue-final IOC partial fills are accepted
|
|
308
|
+
// A partial close (venue-final IOC partial fills are accepted) gets ONE residual
|
|
304
309
|
// close (closePerpVerified); a residual still open fails the step with ORDER_PARTIAL.
|
|
305
310
|
const liveSize = pos.size.replace(/^-/, "");
|
|
306
311
|
const c = await closePerpVerified(adapter, adapter.name, pos.symbol, closeSide, liveSize);
|
package/dist/rebalance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PerpError } from "./errors.js";
|
|
2
2
|
import { getVenue, tryResolveVenueId } from "./exchanges/venue.js";
|
|
3
3
|
import "./exchanges/venues/index.js";
|
|
4
|
-
/** Settlement chains with no withdraw → bridge → deposit route in this CLI
|
|
4
|
+
/** Settlement chains with no withdraw → bridge → deposit route in this CLI. */
|
|
5
5
|
const NO_ROUTE_CHAINS = new Set(["robinhood", "offchain"]);
|
|
6
6
|
/** Throws NO_BRIDGE_ROUTE when the venue's funds live where no bridge route exists. */
|
|
7
7
|
export function assertBridgeRoute(exchange) {
|
package/dist/risk.d.ts
CHANGED
|
@@ -52,7 +52,8 @@ export interface RiskAssessment {
|
|
|
52
52
|
marginUtilization: number;
|
|
53
53
|
largestPositionUsd: number;
|
|
54
54
|
maxLeverageUsed: number;
|
|
55
|
-
|
|
55
|
+
/** null = no position with a known liquidation price (no positions, or none reported) — never a -1 sentinel */
|
|
56
|
+
minLiquidationDistancePct: number | null;
|
|
56
57
|
};
|
|
57
58
|
liquidationDistances: LiquidationDistanceInfo[];
|
|
58
59
|
limits: RiskLimits;
|
package/dist/risk.js
CHANGED
|
@@ -159,9 +159,9 @@ export function assessRisk(balances, positions, limits) {
|
|
|
159
159
|
}
|
|
160
160
|
// Check liquidation distances
|
|
161
161
|
const liquidationDistances = getLiquidationDistances(positions, lim);
|
|
162
|
-
let minLiquidationDistancePct =
|
|
162
|
+
let minLiquidationDistancePct = null;
|
|
163
163
|
for (const ld of liquidationDistances) {
|
|
164
|
-
if (ld.distancePct < minLiquidationDistancePct) {
|
|
164
|
+
if (minLiquidationDistancePct === null || ld.distancePct < minLiquidationDistancePct) {
|
|
165
165
|
minLiquidationDistancePct = ld.distancePct;
|
|
166
166
|
}
|
|
167
167
|
if (ld.distancePct < lim.minLiquidationDistance) {
|
|
@@ -174,8 +174,6 @@ export function assessRisk(balances, positions, limits) {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
if (minLiquidationDistancePct === Infinity)
|
|
178
|
-
minLiquidationDistancePct = -1; // no positions
|
|
179
177
|
// Determine overall risk level
|
|
180
178
|
let level = "low";
|
|
181
179
|
if (violations.some(v => v.severity === "critical"))
|
package/dist/settings.d.ts
CHANGED
|
@@ -168,7 +168,36 @@ export interface QfexAgentMeta {
|
|
|
168
168
|
approvedAt: string;
|
|
169
169
|
source: "imported";
|
|
170
170
|
}
|
|
171
|
-
|
|
171
|
+
/**
|
|
172
|
+
* RISEx session signer (EVM key). Registered by the master with RegisterSigner (+ the signer's
|
|
173
|
+
* VerifySigner) and then restricted ON-CHAIN to the trading permission (DisablePermission for every
|
|
174
|
+
* other permission, signed by the signer and sent by the master). The private key lives only in OWS wallet `agentWalletName`
|
|
175
|
+
* (SSOT Rule #3).
|
|
176
|
+
*/
|
|
177
|
+
export interface RisexAgentMeta {
|
|
178
|
+
kind: "risex";
|
|
179
|
+
agentName: string;
|
|
180
|
+
agentWalletName: string;
|
|
181
|
+
/** Signer (session key) address. */
|
|
182
|
+
signerAddress: `0x${string}`;
|
|
183
|
+
/** The RISEx account (master wallet address) the signer trades for. */
|
|
184
|
+
accountAddress: `0x${string}`;
|
|
185
|
+
/** ISO timestamp of the on-chain expiration signed in RegisterSigner. */
|
|
186
|
+
expiresAt: string;
|
|
187
|
+
/**
|
|
188
|
+
* On-chain permission state proven after registration (Authorization.hasPermission):
|
|
189
|
+
* trading-only = only the router permission enabled. Never written from a guess.
|
|
190
|
+
*/
|
|
191
|
+
permissions: {
|
|
192
|
+
tradingOnly: boolean;
|
|
193
|
+
disabled: number[];
|
|
194
|
+
verifiedAt: string | null;
|
|
195
|
+
};
|
|
196
|
+
network: "mainnet" | "testnet";
|
|
197
|
+
approvedAt: string;
|
|
198
|
+
source: "approved";
|
|
199
|
+
}
|
|
200
|
+
export type AnyAgentMeta = AgentMeta | OndoAgentMeta | ArcusAgentMeta | QfexAgentMeta | RisexAgentMeta;
|
|
172
201
|
export interface AgentsByExchange {
|
|
173
202
|
aster?: Record<string, AgentMeta>;
|
|
174
203
|
hyperliquid?: Record<string, AgentMeta>;
|
|
@@ -178,6 +207,7 @@ export interface AgentsByExchange {
|
|
|
178
207
|
ondo?: Record<string, OndoAgentMeta>;
|
|
179
208
|
arcus?: Record<string, ArcusAgentMeta>;
|
|
180
209
|
qfex?: Record<string, QfexAgentMeta>;
|
|
210
|
+
risex?: Record<string, RisexAgentMeta>;
|
|
181
211
|
}
|
|
182
212
|
export interface Settings {
|
|
183
213
|
/** Default exchange when -e flag is omitted */
|
|
@@ -203,8 +233,7 @@ export interface Settings {
|
|
|
203
233
|
/** OWS-native agent wallets registered per exchange (Phase 2a: Aster only) */
|
|
204
234
|
agents?: AgentsByExchange;
|
|
205
235
|
/**
|
|
206
|
-
* Per-venue slippage bound override (basis points) for market-type orders
|
|
207
|
-
* (design §13.2). Absent key = use `defaultSlippageBps`.
|
|
236
|
+
* Per-venue slippage bound override (basis points) for market-type orders. Absent key = use `defaultSlippageBps`.
|
|
208
237
|
* Resolved only by resolveSlippage() (src/exchanges/slippage.ts).
|
|
209
238
|
*/
|
|
210
239
|
slippageBps: {
|
package/dist/signer/ows-evm.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class OwsEvmSigner implements EvmSigner {
|
|
|
17
17
|
}>>, value: Record<string, unknown>): Promise<string>;
|
|
18
18
|
/**
|
|
19
19
|
* Sign an unsigned EVM transaction (on-chain txs sent by the master wallet — Lighter RH
|
|
20
|
-
* deposit
|
|
20
|
+
* deposit). The digest is computed HERE from the transaction (`unsignedHash`),
|
|
21
21
|
* so callers can never feed an arbitrary 32-byte hash to the master key. Returns the
|
|
22
22
|
* 65-byte 0x signature; the caller attaches it and checks the recovered sender.
|
|
23
23
|
*/
|
package/dist/signer/ows-evm.js
CHANGED
|
@@ -44,7 +44,7 @@ export class OwsEvmSigner {
|
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Sign an unsigned EVM transaction (on-chain txs sent by the master wallet — Lighter RH
|
|
47
|
-
* deposit
|
|
47
|
+
* deposit). The digest is computed HERE from the transaction (`unsignedHash`),
|
|
48
48
|
* so callers can never feed an arbitrary 32-byte hash to the master key. Returns the
|
|
49
49
|
* 65-byte 0x signature; the caller attaches it and checks the recovered sender.
|
|
50
50
|
*/
|
package/dist/smart-order.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - Saves 5-50+ bps vs raw market orders on thin books
|
|
17
17
|
* - Predictable execution price
|
|
18
18
|
*
|
|
19
|
-
* Order-outcome typing
|
|
19
|
+
* Order-outcome typing: every failure BEFORE the IOC is sent (orderbook read,
|
|
20
20
|
* empty book, unusable price), an explicit venue rejection embedded in the response, and an
|
|
21
21
|
* explicit IOC no-match (nothing filled, nothing rests) throw a definite ORDER_REJECTED
|
|
22
22
|
* (orderNotSent), so placePerpVerified does not quarantine a leg that never traded. Anything
|
package/dist/smart-order.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - Saves 5-50+ bps vs raw market orders on thin books
|
|
17
17
|
* - Predictable execution price
|
|
18
18
|
*
|
|
19
|
-
* Order-outcome typing
|
|
19
|
+
* Order-outcome typing: every failure BEFORE the IOC is sent (orderbook read,
|
|
20
20
|
* empty book, unusable price), an explicit venue rejection embedded in the response, and an
|
|
21
21
|
* explicit IOC no-match (nothing filled, nothing rests) throw a definite ORDER_REJECTED
|
|
22
22
|
* (orderNotSent), so placePerpVerified does not quarantine a leg that never traded. Anything
|
|
@@ -13,7 +13,7 @@ export interface FundingArbParams {
|
|
|
13
13
|
maxDrawdown?: number;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* `perp strategy funding-arb` loop. With autoExecute
|
|
16
|
+
* `perp strategy funding-arb` loop. With autoExecute:
|
|
17
17
|
* - entry = openPerpPairVerified (long, then the short sized to the long's VERIFIED fill;
|
|
18
18
|
* definite short failure → reduce-only rollback; unknown outcome → quarantine). A lot-size
|
|
19
19
|
* rejection is never resubmitted blindly: the long is re-sized only when it was definitely
|
|
@@ -72,7 +72,7 @@ async function fetchAllRates() {
|
|
|
72
72
|
}
|
|
73
73
|
// annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
|
|
74
74
|
/**
|
|
75
|
-
* `perp strategy funding-arb` loop. With autoExecute
|
|
75
|
+
* `perp strategy funding-arb` loop. With autoExecute:
|
|
76
76
|
* - entry = openPerpPairVerified (long, then the short sized to the long's VERIFIED fill;
|
|
77
77
|
* definite short failure → reduce-only rollback; unknown outcome → quarantine). A lot-size
|
|
78
78
|
* rejection is never resubmitted blindly: the long is re-sized only when it was definitely
|
package/dist/trade-validator.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function validateTrade(adapter, params) {
|
|
|
12
12
|
const checks = [];
|
|
13
13
|
const warnings = [];
|
|
14
14
|
const sym = params.symbol.toUpperCase();
|
|
15
|
-
// Liquidity threshold = the bound a market order on this venue would carry
|
|
15
|
+
// Liquidity threshold = the bound a market order on this venue would carry.
|
|
16
16
|
const maxSlippage = resolveAdapterSlippage(adapter, null).bps / 10_000;
|
|
17
17
|
// Fetch market list, balance, and positions in parallel.
|
|
18
18
|
// Defer the orderbook call until symbol membership is confirmed so invalid
|
|
@@ -69,7 +69,12 @@ export async function validateTrade(adapter, params) {
|
|
|
69
69
|
// reject an order sized past a threshold even though the flat value allows it,
|
|
70
70
|
// so resolve the cap AT THIS ORDER'S NOTIONAL.
|
|
71
71
|
const effectiveMax = maxLeverageForNotional(market, notional);
|
|
72
|
-
|
|
72
|
+
// A venue that publishes no max leverage (Aster's public exchangeInfo): the margin estimate
|
|
73
|
+
// uses 1x — the most conservative requirement — and says so; the venue enforces its own cap.
|
|
74
|
+
if (effectiveMax === null && params.leverage === undefined) {
|
|
75
|
+
warnings.push(`Max leverage for ${sym} is not published by ${adapter.name} — margin estimated at 1x; pass --leverage for a tighter estimate`);
|
|
76
|
+
}
|
|
77
|
+
const leverage = params.leverage ?? effectiveMax ?? 1;
|
|
73
78
|
if (!Number.isFinite(leverage) || leverage <= 0) {
|
|
74
79
|
throw new Error(`No usable leverage for ${sym} (resolved=${leverage}); supply params.leverage explicitly.`);
|
|
75
80
|
}
|
|
@@ -197,7 +202,7 @@ export async function validateTrade(adapter, params) {
|
|
|
197
202
|
const fee = notional * DEFAULT_TAKER_FEE;
|
|
198
203
|
const estimatedSlippage = params.type === "limit" ? 0 : notional * 0.001;
|
|
199
204
|
const valid = checks.every(c => c.passed);
|
|
200
|
-
if (leverage > effectiveMax) {
|
|
205
|
+
if (effectiveMax !== null && market.maxLeverage !== null && leverage > effectiveMax) {
|
|
201
206
|
// Compare against the tier that applies at this notional, not the headline
|
|
202
207
|
// cap — otherwise the check is tautological whenever leverage defaulted FROM
|
|
203
208
|
// maxLeverage, and it never fires for the tiered case it exists to catch.
|
|
@@ -259,7 +264,7 @@ export async function enforceOrderRisk(adapter, p) {
|
|
|
259
264
|
const markets = await adapter.getMarkets();
|
|
260
265
|
const m = markets.find((mk) => symbolMatch(mk.symbol, p.symbol));
|
|
261
266
|
if (!m) {
|
|
262
|
-
throw new PerpError("SYMBOL_NOT_FOUND", `${p.symbol} not found on ${adapter.name}`, { exchange: adapter.name });
|
|
267
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `${p.symbol} not found on ${adapter.name}`, { exchange: adapter.name, remediation: `List markets: perp -e ${adapter.name} market list` });
|
|
263
268
|
}
|
|
264
269
|
price = Number(m.markPrice);
|
|
265
270
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { OrderResult } from "./exchanges/venue-types.js";
|
|
2
2
|
export declare function symbolMatch(candidate: string, target: string): boolean;
|
|
3
3
|
export declare function formatUsd(value: string | number): string;
|
|
4
|
+
/**
|
|
5
|
+
* Symbol argument as typed, normalized for display and lookup: upper case, except a HIP-3 dex
|
|
6
|
+
* prefix, which is lower case on the venue ("xyz:xyz100" / "XYZ:XYZ100" → "xyz:XYZ100").
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeSymbolArg(symbol: string): string;
|
|
4
9
|
export declare function formatPnl(value: string | number): string;
|
|
5
10
|
export declare function formatPercent(value: string | number): string;
|
|
6
11
|
export declare function makeTable(head: string[], rows: string[][]): string;
|
|
@@ -26,6 +31,10 @@ export interface ApiResponse<T = unknown> {
|
|
|
26
31
|
exchange?: string;
|
|
27
32
|
timestamp: string;
|
|
28
33
|
duration_ms?: number;
|
|
34
|
+
skippedRows?: {
|
|
35
|
+
count: number;
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
29
38
|
};
|
|
30
39
|
}
|
|
31
40
|
/** Wrap successful result in standard envelope */
|
|
@@ -50,18 +59,18 @@ export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>,
|
|
|
50
59
|
/**
|
|
51
60
|
* Central CLI error printer. JSON: the structured envelope incl. details and
|
|
52
61
|
* remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
|
|
53
|
-
*
|
|
62
|
+
* — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
|
|
54
63
|
* otherwise `Error: <message>` plus the remediation.
|
|
55
64
|
*/
|
|
56
65
|
export declare function printCliError(err: unknown, isJson: boolean, exchange?: string): void;
|
|
57
66
|
/**
|
|
58
|
-
* JSON payload of an order result
|
|
67
|
+
* JSON payload of an order result: the uniform OrderResult (+ execution)
|
|
59
68
|
* with the venue's raw response duplicated under `legacy` for one release.
|
|
60
69
|
*/
|
|
61
70
|
export declare function orderResultJson(r: OrderResult & {
|
|
62
71
|
remediation?: string | null;
|
|
63
72
|
}, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
64
|
-
/** Human lines for an order result — one format for every venue
|
|
73
|
+
/** Human lines for an order result — one format for every venue. */
|
|
65
74
|
export declare function formatOrderResult(r: OrderResult & {
|
|
66
75
|
remediation?: string | null;
|
|
67
76
|
}): string[];
|
package/dist/utils.js
CHANGED
|
@@ -30,6 +30,14 @@ export function formatUsd(value) {
|
|
|
30
30
|
maximumFractionDigits: maxDecimals,
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Symbol argument as typed, normalized for display and lookup: upper case, except a HIP-3 dex
|
|
35
|
+
* prefix, which is lower case on the venue ("xyz:xyz100" / "XYZ:XYZ100" → "xyz:XYZ100").
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeSymbolArg(symbol) {
|
|
38
|
+
const i = symbol.indexOf(":");
|
|
39
|
+
return i === -1 ? symbol.toUpperCase() : `${symbol.slice(0, i).toLowerCase()}:${symbol.slice(i + 1).toUpperCase()}`;
|
|
40
|
+
}
|
|
33
41
|
export function formatPnl(value) {
|
|
34
42
|
const num = Number(value);
|
|
35
43
|
if (isNaN(num))
|
|
@@ -235,7 +243,7 @@ export async function withJsonErrors(isJson, fn, exchange) {
|
|
|
235
243
|
/**
|
|
236
244
|
* Central CLI error printer. JSON: the structured envelope incl. details and
|
|
237
245
|
* remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
|
|
238
|
-
*
|
|
246
|
+
* — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
|
|
239
247
|
* otherwise `Error: <message>` plus the remediation.
|
|
240
248
|
*/
|
|
241
249
|
export function printCliError(err, isJson, exchange) {
|
|
@@ -262,13 +270,13 @@ export function printCliError(err, isJson, exchange) {
|
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
272
|
/**
|
|
265
|
-
* JSON payload of an order result
|
|
273
|
+
* JSON payload of an order result: the uniform OrderResult (+ execution)
|
|
266
274
|
* with the venue's raw response duplicated under `legacy` for one release.
|
|
267
275
|
*/
|
|
268
276
|
export function orderResultJson(r, extra) {
|
|
269
277
|
return { ...r, ...extra, legacy: r.venueRaw };
|
|
270
278
|
}
|
|
271
|
-
/** Human lines for an order result — one format for every venue
|
|
279
|
+
/** Human lines for an order result — one format for every venue. */
|
|
272
280
|
export function formatOrderResult(r) {
|
|
273
281
|
const lines = [];
|
|
274
282
|
const statusColor = r.status === "filled" ? chalk.green
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Venue connectivity probe — the one implementation behind `perp health`,
|
|
3
|
+
* `perp portfolio --health` and the MCP `health_check` tool.
|
|
4
|
+
*
|
|
5
|
+
* Pacifica / Hyperliquid / Lighter / Aster / QFEX / GRVT: a REST ping (status = HTTP status, 0 = no
|
|
6
|
+
* response). Every other venue — including read-only (market-data-only) venues such as TxFlow —
|
|
7
|
+
* is probed with its public market-data read (status null). A venue with no environment on the
|
|
8
|
+
* selected network (TxFlow has no testnet) is not probed and is listed in `skipped`.
|
|
9
|
+
*
|
|
10
|
+
* The ping helpers (shared-api) read the shared API URLs, which follow the network the caller
|
|
11
|
+
* selected (setSharedApiNetwork); Aster / QFEX / market-data probes take `network` directly.
|
|
12
|
+
*/
|
|
13
|
+
import type { Network, VenueId } from "./exchanges/venue-types.js";
|
|
14
|
+
import "./exchanges/venues/index.js";
|
|
15
|
+
export interface VenueProbe {
|
|
16
|
+
venue: VenueId;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
latencyMs: number;
|
|
19
|
+
/** HTTP status of a REST ping (0 = no response); null for a market-data probe. */
|
|
20
|
+
status: number | null;
|
|
21
|
+
/** null when ok. */
|
|
22
|
+
error: string | null;
|
|
23
|
+
probe: "ping" | "markets";
|
|
24
|
+
/** "unofficial" = the venue is read through an undocumented API (VenueDefinition.apiStatus). */
|
|
25
|
+
apiStatus: "official" | "unofficial";
|
|
26
|
+
}
|
|
27
|
+
export interface VenueHealth {
|
|
28
|
+
probes: VenueProbe[];
|
|
29
|
+
/** Venues requested but not probed: no environment on `network`. */
|
|
30
|
+
skipped: Array<{
|
|
31
|
+
venue: VenueId;
|
|
32
|
+
reason: string;
|
|
33
|
+
}>;
|
|
34
|
+
allHealthy: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Probe `venueIds` (default: every registered venue) on `network`, in parallel. Never throws for a
|
|
38
|
+
* venue failure — each failure is a probe with ok:false and its message.
|
|
39
|
+
*/
|
|
40
|
+
export declare function probeVenueHealth(network: Network, venueIds?: readonly string[]): Promise<VenueHealth>;
|