perp-cli 0.15.0 → 0.16.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.
Files changed (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. package/dist/cli-spec.js +0 -81
@@ -1,9 +1,14 @@
1
1
  import { listConfiguredAccountVenues } from "../config.js";
2
- import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch } from "../utils.js";
2
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch, withJsonErrors } from "../utils.js";
3
3
  import chalk from "chalk";
4
4
  import { hasPacificaSdk, isDexCapable } from "../exchanges/capabilities.js";
5
5
  import { PerpError } from "../errors.js";
6
6
  import { unsupported } from "../exchanges/trading-v2.js";
7
+ /** The configured Pacifica account of an account read; keyless → NO_SIGNER_AVAILABLE (never the throwaway keypair's). */
8
+ function pacificaAccount(adapter) {
9
+ const read = adapter.accountForReads;
10
+ return typeof read === "function" ? read.call(adapter) : adapter.publicKey;
11
+ }
7
12
  function pac(adapter) {
8
13
  if (!hasPacificaSdk(adapter))
9
14
  throw new Error("Market settings are only available on Pacifica.");
@@ -28,8 +33,14 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
28
33
  // user what to run and exit non-zero so scripts can detect the rename.
29
34
  account
30
35
  .command("balance")
31
- .description("[renamed] Use 'perp portfolio' (exchange-aggregated) or 'perp portfolio -e <ex>'")
32
- .action(async () => {
36
+ .description("Any account's balances by public address, keyless (--address). Your own balance: 'perp portfolio' (renamed in v0.12)")
37
+ .option("--address <address>", "Read this public address — keyless (hyperliquid, lighter, lighter-rh, risex: 0x address; pacifica: Solana address). Without -e: every venue that supports it")
38
+ .option("--account-index <n>", "With --address on Lighter: only this account (sub-account) of the address")
39
+ .action(async (opts) => {
40
+ if (opts.address !== undefined) {
41
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
42
+ return void await withJsonErrors(isJson(), () => balanceByAddress(opts.address, explicitExchange ? String(program.opts().exchange) : null, opts));
43
+ }
33
44
  const msg = "'account balance' was renamed in v0.12 — use 'portfolio' instead.";
34
45
  const remediation = "perp portfolio # all exchanges\nperp portfolio -e <exchange> # single exchange detail";
35
46
  if (isJson()) {
@@ -47,9 +58,8 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
47
58
  async function fetchHip3Data(hlAdapter, fetcher) {
48
59
  const dexes = await hlAdapter.listDeployedDexes();
49
60
  const results = await Promise.allSettled(dexes.map(async (d) => {
50
- // Clone adapter with dex set
51
- const dexAdapter = Object.create(hlAdapter);
52
- dexAdapter.setDex(d.name);
61
+ // An independent per-dex adapter (a prototype copy + setDex emptied the native adapter's maps)
62
+ const dexAdapter = hlAdapter.forDex(d.name);
53
63
  const items = await fetcher(dexAdapter);
54
64
  return { dex: d.name, items };
55
65
  }));
@@ -58,12 +68,158 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
58
68
  .map(r => r.value)
59
69
  .filter(r => r.items.length > 0);
60
70
  }
71
+ /** `account balance --address`: keyless public read (registry: VenuePublicData.fetchBalanceByAddress). */
72
+ async function balanceByAddress(address, venue, opts) {
73
+ const { readBalanceByAddress, readBalanceByAddressAll } = await import("../balance-by-address.js");
74
+ const { networkOf } = await import("../cli-flags.js");
75
+ let accountIndex;
76
+ if (opts.accountIndex !== undefined) {
77
+ if (!/^\d+$/.test(opts.accountIndex.trim()))
78
+ throw new PerpError("INVALID_PARAMS", `--account-index must be a whole number, got "${opts.accountIndex}"`);
79
+ accountIndex = Number(opts.accountIndex);
80
+ }
81
+ const net = networkOf(program);
82
+ const balanceRows = (d) => {
83
+ const row = (label, b) => [
84
+ label, `$${formatUsd(b.equity)}`, `$${formatUsd(b.available)}`, `$${formatUsd(b.marginUsed)}`, formatPnl(b.unrealizedPnl),
85
+ ];
86
+ return [
87
+ ...(d.balance !== null ? [row(d.accountMode !== undefined ? `native (${d.accountMode})` : "account", d.balance)] : []),
88
+ ...(d.accounts ?? []).map(a => row(`account ${a.accountIndex}`, a.balance)),
89
+ ...Object.entries(d.dexes ?? {}).map(([dex, b]) => row(chalk.magenta(`dex ${dex}`), b)),
90
+ ];
91
+ };
92
+ const spotRows = (d) => [
93
+ ...(d.spot ?? []).map(s => ["-", s.token, s.total, s.available, s.held]),
94
+ ...(d.accounts ?? []).flatMap(a => a.spot.map(s => [String(a.accountIndex), s.token, s.total, s.available, s.held])),
95
+ ];
96
+ const extras = (d) => {
97
+ const b = d.balance;
98
+ if (b === null)
99
+ return [];
100
+ return ["usdcBalance", "borrowedUsdc", "pendingInterest", "availableToWithdraw"].filter(k => k in b).map(k => `${k}: ${b[k] === null ? "-" : String(b[k])}`);
101
+ };
102
+ const printVenue = (ex, d) => {
103
+ console.log(chalk.cyan.bold(`\n ${ex}`));
104
+ const rows = balanceRows(d);
105
+ if (rows.length > 0)
106
+ console.log(makeTable(["Account", "Equity", "Available", "Margin Used", "uPnL"], rows));
107
+ const x = extras(d);
108
+ if (x.length > 0)
109
+ console.log(chalk.gray(` ${x.join(" ")}`));
110
+ const sr = spotRows(d);
111
+ if (sr.length > 0)
112
+ console.log(makeTable(["Acct", "Spot token", "Total", "Available", "Held"], sr));
113
+ if (rows.length === 0 && sr.length === 0)
114
+ console.log(chalk.gray(" no balance (see notes)"));
115
+ };
116
+ if (venue !== null) {
117
+ const r = await readBalanceByAddress(venue, net, address, { accountIndex });
118
+ if (isJson()) {
119
+ return printJson(jsonOk({ exchange: r.venue, address, ...r.data }, {
120
+ exchange: r.venue, address, source: "public",
121
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
122
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
123
+ }));
124
+ }
125
+ for (const w of r.warnings)
126
+ console.log(chalk.yellow(` ${w}`));
127
+ printVenue(r.venue, r.data);
128
+ for (const n of r.notes)
129
+ console.log(chalk.gray(` ${n}`));
130
+ console.log();
131
+ return;
132
+ }
133
+ const all = await readBalanceByAddressAll(net, address, { accountIndex });
134
+ if (isJson()) {
135
+ return printJson(jsonOk({
136
+ venues: all.venues,
137
+ ...(Object.keys(all.errors).length > 0 ? { errors: all.errors } : {}),
138
+ ...(Object.keys(all.notes).length > 0 ? { notes: all.notes } : {}),
139
+ }, { address, source: "public", ...(all.warnings.length > 0 ? { warnings: all.warnings } : {}) }));
140
+ }
141
+ for (const [ex, d] of Object.entries(all.venues))
142
+ printVenue(ex, d);
143
+ console.log();
144
+ for (const [ex, e] of Object.entries(all.errors))
145
+ console.log(chalk.gray(` ${ex}: ${e.code} ${e.message}`));
146
+ for (const [ex, n] of Object.entries(all.notes))
147
+ console.log(chalk.gray(` ${ex}: ${n.join("; ")}`));
148
+ }
149
+ /** `account positions --address`: keyless public read (registry: VenuePublicData.fetchPositionsByAddress). */
150
+ async function positionsByAddress(address, venue, opts) {
151
+ const { readPositionsByAddress, readPositionsByAddressAll } = await import("../positions-by-address.js");
152
+ const { networkOf } = await import("../cli-flags.js");
153
+ let accountIndex;
154
+ if (opts.accountIndex !== undefined) {
155
+ if (!/^\d+$/.test(opts.accountIndex.trim()))
156
+ throw new PerpError("INVALID_PARAMS", `--account-index must be a whole number, got "${opts.accountIndex}"`);
157
+ accountIndex = Number(opts.accountIndex);
158
+ }
159
+ const net = networkOf(program);
160
+ const row = (p, ex) => [
161
+ chalk.white.bold(p.symbol),
162
+ ...(ex !== null ? [chalk.gray(ex)] : []),
163
+ p.dex != null ? chalk.magenta(p.dex) : p.accountIndex !== undefined ? chalk.gray(String(p.accountIndex)) : chalk.gray("-"),
164
+ p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
165
+ p.size,
166
+ `$${formatUsd(p.entryPrice)}`,
167
+ `$${formatUsd(p.markPrice)}`,
168
+ p.liquidationPrice === null ? chalk.gray("-") : `$${formatUsd(p.liquidationPrice)}`,
169
+ formatPnl(p.unrealizedPnl),
170
+ `${p.leverage}x`,
171
+ ];
172
+ if (venue !== null) {
173
+ const r = await readPositionsByAddress(venue, net, address, { accountIndex, symbol: opts.symbol });
174
+ if (isJson()) {
175
+ return printJson(jsonOk(r.positions, {
176
+ exchange: r.venue, address, source: "public",
177
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
178
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
179
+ }));
180
+ }
181
+ for (const w of r.warnings)
182
+ console.log(chalk.yellow(` ${w}`));
183
+ if (r.positions.length === 0) {
184
+ console.log(chalk.gray(`\n No open positions for ${address} on ${r.venue}${r.notes.length > 0 ? ` (${r.notes.join("; ")})` : ""}.\n`));
185
+ return;
186
+ }
187
+ console.log(makeTable(["Symbol", "Dex/Acct", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], r.positions.map(p => row(p, null))));
188
+ return;
189
+ }
190
+ const all = await readPositionsByAddressAll(net, address, { accountIndex, symbol: opts.symbol });
191
+ if (isJson()) {
192
+ return printJson(jsonOk({
193
+ venues: all.venues,
194
+ ...(Object.keys(all.errors).length > 0 ? { errors: all.errors } : {}),
195
+ ...(Object.keys(all.notes).length > 0 ? { notes: all.notes } : {}),
196
+ }, { address, source: "public", ...(all.warnings.length > 0 ? { warnings: all.warnings } : {}) }));
197
+ }
198
+ const rows = Object.entries(all.venues).flatMap(([ex, ps]) => ps.map(p => row(p, ex)));
199
+ if (rows.length > 0)
200
+ console.log(makeTable(["Symbol", "Exch", "Dex/Acct", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], rows));
201
+ else
202
+ console.log(chalk.gray(`\n No open positions for ${address} on ${Object.keys(all.venues).join(", ") || "any venue"}.\n`));
203
+ for (const [ex, e] of Object.entries(all.errors))
204
+ console.log(chalk.gray(` ${ex}: ${e.code} ${e.message}`));
205
+ for (const [ex, n] of Object.entries(all.notes))
206
+ console.log(chalk.gray(` ${ex}: ${n.join("; ")}`));
207
+ }
61
208
  account
62
209
  .command("positions")
63
- .description("Show open positions")
210
+ .description("Show open positions (yours; or any account's, keyless, with --address)")
64
211
  .option("--hip3", "Include HIP-3 dex positions (Hyperliquid)")
212
+ .option("--address <address>", "Read this public address instead of your account — keyless (hyperliquid, lighter, lighter-rh, risex: 0x address; pacifica: Solana address). Without -e: every venue that supports it")
213
+ .option("--account-index <n>", "With --address on Lighter: only this account (sub-account) of the address")
214
+ .option("--symbol <symbol>", "With --address: only this market (plain asset, e.g. BTC)")
65
215
  .action(async (opts) => {
66
216
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
217
+ if (opts.address !== undefined) {
218
+ return void await withJsonErrors(isJson(), () => positionsByAddress(opts.address, explicitExchange ? String(program.opts().exchange) : null, opts));
219
+ }
220
+ if (opts.accountIndex !== undefined || opts.symbol !== undefined) {
221
+ throw new PerpError("INVALID_PARAMS", "--account-index / --symbol apply to --address reads", { remediation: "Add --address <address>, or drop the option" });
222
+ }
67
223
  if (!explicitExchange && getAdapterForExchange) {
68
224
  const EXCHANGES = await listConfiguredAccountVenues();
69
225
  const allRows = [];
@@ -316,7 +472,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
316
472
  // Pacifica: use dedicated SDK endpoint
317
473
  if (hasPacificaSdk(adapter)) {
318
474
  const sdk = adapter.sdk;
319
- const settings = await sdk.getAccountSettings(adapter.publicKey);
475
+ const settings = await sdk.getAccountSettings(pacificaAccount(adapter));
320
476
  if (isJson())
321
477
  return printJson(jsonOk(settings));
322
478
  // GET /account/settings returns an object; per-market entries are in `margin_settings`
@@ -404,7 +560,8 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
404
560
  .description("Margin details for a specific symbol position")
405
561
  .action(async (symbol) => {
406
562
  const sym = symbol.toUpperCase();
407
- try {
563
+ // Errors: the one envelope (code, status, remediation) and exit 1 via withJsonErrors.
564
+ return void await withJsonErrors(isJson(), async () => {
408
565
  const adapter = await getAdapter();
409
566
  const [balance, positions] = await Promise.all([
410
567
  adapter.getBalance(),
@@ -452,20 +609,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
452
609
  console.log(` Liquidation: ${pos.liquidationPrice === null ? chalk.gray("-") : `$${formatUsd(pos.liquidationPrice)}`}`);
453
610
  console.log(` Unrealized PnL: ${formatPnl(pos.unrealizedPnl)}`);
454
611
  console.log();
455
- }
456
- catch (err) {
457
- const msg = err instanceof Error ? err.message : String(err);
458
- if (isJson()) {
459
- const { classifyError } = await import("../errors.js");
460
- const classified = classifyError(err);
461
- return printJson(jsonError(classified.code, classified.message, {
462
- status: classified.status,
463
- retryable: classified.retryable,
464
- }));
465
- }
466
- console.error(chalk.red(`Error: ${msg}`));
467
- process.exitCode = 1;
468
- }
612
+ });
469
613
  });
470
614
  // ── PnL Report ──
471
615
  account
@@ -609,7 +753,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
609
753
  }
610
754
  const p = pac(adapter);
611
755
  const sdk = p.sdk;
612
- const orders = await sdk.getTWAPOrders(p.publicKey);
756
+ const orders = await sdk.getTWAPOrders(pacificaAccount(p));
613
757
  if (isJson())
614
758
  return printJson(jsonOk(orders));
615
759
  if (!orders || orders.length === 0) {
@@ -1,10 +1,11 @@
1
1
  import { isDryRun, networkOf, supportsDryRun } from "../../cli-flags.js";
2
2
  import chalk from "chalk";
3
- import { makeTable, formatUsd, formatPnl, printJson, jsonOk, logSettledRejections } from "../../utils.js";
3
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
4
4
  import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/index.js";
5
5
  import { readExecutionLog, logExecution } from "../../execution-log.js";
6
6
  import { toHourlyRate } from "../../funding.js";
7
- import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
7
+ import { fetchVenuesRates } from "../../funding/rates.js";
8
+ import { sharedApiNetwork } from "../../api/public/urls.js";
8
9
  import { computeBasisRisk } from "../../arb-utils.js";
9
10
  import { removePosition as persistRemovePosition, getPositions as getArbStatePositions, loadArbState } from "../../arb-state.js";
10
11
  import { fetchBalancesWithFailures, computeRebalancePlan } from "../../rebalance.js";
@@ -15,7 +16,7 @@ import { listConfiguredAccountVenues } from "../../config.js";
15
16
  import { closePerpPairVerified, handleSpotClose } from "../arb-auto.js";
16
17
  import { quarantineEntryFor } from "../../bot/quarantine.js";
17
18
  import { PerpError, classifyError, parsePositiveDecimal } from "../../errors.js";
18
- import { listVenuesWithRole, resolveVenueId } from "../../exchanges/venue.js";
19
+ import { getVenue, listVenuesWithRole, resolveVenueId } from "../../exchanges/venue.js";
19
20
  // ── Helpers ──
20
21
  import { DEFAULT_TAKER_FEE as TAKER_FEE } from "../../constants.js";
21
22
  /**
@@ -108,81 +109,22 @@ function entryHourly(e) {
108
109
  return e.fundingHours !== null ? e.rate / e.fundingHours : toHourlyRate(e.rate, e.exchange);
109
110
  }
110
111
  async function fetchFundingRatesMap() {
112
+ // The SAME rate rows as `arb scan --rates` (fetchVenuesRates): canonical-keyed (unclassified /
113
+ // ambiguous tickers have no row), each with its own basis (fundingHours) and rateKind. A failed
114
+ // venue is reported on stderr (explicit, never silent).
115
+ const venues = ["pacifica", "hyperliquid", "lighter", "aster"].map(id => getVenue(id));
116
+ const { rates: fetched, exchangeErrors } = await fetchVenuesRates(venues, sharedApiNetwork(), false);
117
+ // Never a leg on an unverified rate (row label; the venue filter already drops them).
118
+ const rates = fetched.filter(r => r.fundingVerified);
119
+ for (const [ex, msg] of Object.entries(exchangeErrors))
120
+ process.stderr.write(`[arb] ${ex}: funding rates unavailable — ${msg}\n`);
111
121
  const rateMap = new Map();
112
- // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
113
- // alive when one DEX errors out, while rejection reasons get logged to
114
- // stderr (explicit failure, never silent .catch(() => [])).
115
- const settled = await Promise.allSettled([
116
- fetchPacificaPrices(),
117
- fetchHyperliquidMeta(),
118
- fetchLighterOrderBookDetails(),
119
- fetchLighterFundingRates(),
120
- fetch("https://fapi.asterdex.com/fapi/v1/premiumIndex").then(r => r.json()),
121
- fetch("https://fapi.asterdex.com/fapi/v3/fundingInfo").then(r => r.json()),
122
- ]);
123
- logSettledRejections(settled, ["pacifica", "hyperliquid", "lighter:orderbook", "lighter:funding", "aster:premiumIndex", "aster:fundingInfo"], "arb");
124
- const pacAssets = settled[0].status === "fulfilled" ? settled[0].value : [];
125
- const hlAssets = settled[1].status === "fulfilled" ? settled[1].value : [];
126
- const ltDetails = settled[2].status === "fulfilled" ? settled[2].value : [];
127
- const ltFunding = settled[3].status === "fulfilled" ? settled[3].value : [];
128
- // Aster funding basis is per symbol: a rate is usable only with its fundingInfo interval.
129
- const asterPremiums = settled[4].status === "fulfilled" && settled[5].status === "fulfilled" ? settled[4].value : [];
130
- const asterInfo = settled[5].status === "fulfilled" ? settled[5].value : [];
131
- const asterHours = new Map();
132
- for (const row of Array.isArray(asterInfo) ? asterInfo : []) {
133
- const h = Number(row.fundingIntervalHours);
134
- if (typeof row.symbol === "string" && Number.isFinite(h) && h > 0)
135
- asterHours.set(row.symbol, h);
136
- }
137
- const addRate = (sym, exchange, rate, markPrice, fundingHours = null) => {
138
- if (!sym)
139
- return;
140
- if (!rateMap.has(sym))
141
- rateMap.set(sym, []);
142
- rateMap.get(sym).push({ exchange, rate, markPrice, fundingHours });
143
- };
144
- for (const p of pacAssets)
145
- addRate(p.symbol, "pacifica", p.funding, p.mark);
146
- for (const h of hlAssets)
147
- addRate(h.symbol, "hyperliquid", h.funding, h.markPx);
148
- // Lighter: join details + funding by marketId
149
- // SSOT rule #2: skip rows whose mark price cannot be resolved (no 0 fallback).
150
- // Prefer fr.markPrice; fall back to orderBook last-trade as documented
151
- // price-source preference (NOT error fallback).
152
- const ltPriceMap = new Map(ltDetails.map(d => [d.marketId, d.lastTradePrice]));
153
- const ltSymMap = new Map(ltDetails.map(d => [d.marketId, d.symbol]));
154
- for (const fr of ltFunding) {
155
- const sym = fr.symbol || ltSymMap.get(fr.marketId) || "";
156
- if (!sym)
157
- continue;
158
- const directMark = fr.markPrice;
159
- const fallbackMark = ltPriceMap.get(fr.marketId);
160
- let mp;
161
- if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
162
- mp = directMark;
163
- }
164
- else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
165
- mp = fallbackMark;
166
- }
167
- if (mp === undefined || !Number.isFinite(fr.rate))
168
- continue;
169
- addRate(sym, "lighter", fr.rate, mp);
170
- }
171
- // Aster: premiumIndex
172
- // SSOT rule #2: skip rows missing markPrice / lastFundingRate instead of
173
- // 0-coercing — a 0 markPrice would corrupt downstream sizing.
174
- for (const p of asterPremiums) {
175
- const rawSym = String(p.symbol ?? "");
176
- if (!rawSym.endsWith("USDT"))
177
- continue;
178
- const fundingRate = Number(p.lastFundingRate);
179
- const markPrice = Number(p.markPrice);
180
- if (!Number.isFinite(fundingRate) || !Number.isFinite(markPrice) || markPrice <= 0)
181
- continue;
182
- const hours = asterHours.get(rawSym);
183
- if (hours === undefined)
184
- continue; // not in /fapi/v3/fundingInfo → cannot normalize
185
- addRate(rawSym.replace(/USDT$/, ""), "aster", fundingRate, markPrice, hours);
122
+ for (const r of rates) {
123
+ if (r.basis.kind !== "perHours")
124
+ continue; // these four venues quote per hours
125
+ if (!rateMap.has(r.canonical))
126
+ rateMap.set(r.canonical, []);
127
+ rateMap.get(r.canonical).push({ exchange: r.exchange, rate: r.fundingRate, markPrice: r.markPrice, fundingHours: r.basis.hours });
186
128
  }
187
129
  return rateMap;
188
130
  }
@@ -1,6 +1,7 @@
1
1
  import type { Network } from "../exchanges/venue-types.js";
2
2
  import { Command } from "commander";
3
3
  import type { ExchangeAdapter } from "../exchanges/index.js";
4
+ import type { FundingRateKind } from "../exchanges/interface.js";
4
5
  interface FundingSnapshot {
5
6
  symbol: string;
6
7
  pacRate: number;
@@ -17,7 +18,24 @@ interface FundingSnapshot {
17
18
  astMarkPrice: number;
18
19
  /** Aster per-symbol funding interval (= basis and settlement interval) from /fapi/v3/fundingInfo; null when Aster has no rate. */
19
20
  astHours: number | null;
21
+ /** Which rate each paired venue's value is (arb scan rateKind): "lastSettled" | "accruing" | "predicted" | null. */
22
+ rateKinds?: Record<string, FundingRateKind | null>;
23
+ /**
24
+ * Each venue's OWN symbol for this canonical row (Hyperliquid kPEPE, Lighter 1000PEPE): orders,
25
+ * position matching and closes use it, never the canonical key. Absent = the venue trades `symbol`.
26
+ */
27
+ venueSymbols?: Record<string, string>;
20
28
  }
29
+ /** The venue symbol a snapshot / position leg trades on `exchange` (the canonical key when the venue uses it). */
30
+ export declare function legSymbol(row: {
31
+ symbol: string;
32
+ venueSymbols?: Record<string, string>;
33
+ }, exchange: string): string;
34
+ /** --symbols matches a row by its canonical key or any venue's own symbol (kPEPE or 1000PEPE). */
35
+ export declare function matchesSymbolFilter(row: {
36
+ symbol: string;
37
+ venueSymbols?: Record<string, string>;
38
+ }, filter: readonly string[]): boolean;
21
39
  export interface LegOrderMinimums {
22
40
  exchange: string;
23
41
  minSize: string | null;
@@ -216,10 +234,16 @@ export declare function dexCloseBlockedBy(pos: {
216
234
  longDex: string;
217
235
  shortDex: string;
218
236
  }, failedDexes: Record<string, string>): string | null;
237
+ export declare function fetchFundingSpreads(): Promise<FundingSnapshot[]>;
219
238
  export declare let handleSpotClose: ((sym: string, opts: {
220
239
  spotExch?: string;
221
240
  perpExch?: string;
222
241
  }, getAdapterForExchange: (exchange: string) => Promise<import("../exchanges/interface.js").ExchangeAdapter>, isJson: () => boolean) => Promise<void>) | undefined;
242
+ /**
243
+ * `arb auto` flag validation: every numeric flag must be a finite number in its range, `--size` a
244
+ * number or "auto", `--settle-strategy` one of block | aggressive | off. INVALID_PARAMS otherwise.
245
+ */
246
+ export declare function assertArbAutoFlags(opts: Record<string, unknown>): void;
223
247
  export declare function registerArbAutoCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, getHLAdapterForDex?: (dex: string) => Promise<import("../exchanges/hyperliquid.js").HyperliquidAdapter>): void;
224
248
  export declare let handleArbPnlRef: (() => Promise<void>) | undefined;
225
249
  /**