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
@@ -9,8 +9,9 @@
9
9
  * `create`) so that public-data paths never pull in heavy SDKs (lighter WASM).
10
10
  */
11
11
  import { PerpError } from "../errors.js";
12
- import type { ExchangeAdapter } from "./interface.js";
13
- import type { SpotAdapter } from "./spot-interface.js";
12
+ import type { ExchangeAdapter, ExchangeBalance, ExchangePosition } from "./interface.js";
13
+ import { type AddressKind } from "./address.js";
14
+ import type { SpotAdapter, SpotBalance } from "./spot-interface.js";
14
15
  import type { FundingRateBasis, LegacyVenueId, Network, TimeInForce, VenueFundingRate, VenueId, VenueMarket } from "./venue-types.js";
15
16
  import { isVenueId } from "./venue-types.js";
16
17
  import type { AnyAgentMeta } from "../settings.js";
@@ -104,7 +105,75 @@ export interface RateLimitPolicy {
104
105
  }
105
106
  export interface VenuePublicData {
106
107
  fetchFundingRates(net: Network): Promise<VenueFundingRate[]>;
107
- fetchMarkets(net: Network): Promise<VenueMarket[]>;
108
+ /**
109
+ * `opts.funding: false` = the caller needs marks / precision only: a venue whose funding rate
110
+ * costs an extra read (Lighter: the WS market_stats snapshot) skips it (rows fundingRate null).
111
+ */
112
+ fetchMarkets(net: Network, opts?: {
113
+ funding?: boolean;
114
+ }): Promise<VenueMarket[]>;
115
+ /**
116
+ * Keyless open positions of ANY account by its public address (`account positions --address`,
117
+ * MCP get_positions_by_address). Present = the venue supports it; `addressKind` is then the
118
+ * address format it takes (checked before any request). Rows are the keyed `getPositions`
119
+ * mapping (the same mapper) with plain symbols.
120
+ */
121
+ addressKind?: AddressKind;
122
+ fetchPositionsByAddress?(net: Network, address: string, opts: PublicPositionsOptions): Promise<PublicPositionsResult>;
123
+ /**
124
+ * Keyless balances of ANY account by its public address (`account balance --address`, MCP
125
+ * get_balance_by_address). Present = the venue supports it (takes `addressKind`, like
126
+ * fetchPositionsByAddress). The balance is the keyed `getBalance` math (the same function).
127
+ */
128
+ fetchBalanceByAddress?(net: Network, address: string, opts: PublicPositionsOptions): Promise<PublicBalanceResult>;
129
+ }
130
+ /** One Lighter account (sub-account) of an address: its balance and spot assets. */
131
+ export interface PublicAccountBalance {
132
+ accountIndex: number;
133
+ balance: ExchangeBalance;
134
+ spot: SpotBalance[];
135
+ }
136
+ export interface PublicBalanceResult {
137
+ /** The account's balance (the keyed getBalance fields + the venue's extras); null on Lighter (per account in `accounts`). */
138
+ balance: ExchangeBalance | null;
139
+ /** Spot token balances where the venue publishes them by address; null = not published (nothing invented). */
140
+ spot: SpotBalance[] | null;
141
+ /** Lighter / Lighter RH: every account of the address (or the one of accountIndex); null elsewhere. */
142
+ accounts: PublicAccountBalance[] | null;
143
+ /** Hyperliquid: HIP-3 dexes with a non-zero account value (the dex's own perp clearinghouse); null elsewhere. */
144
+ dexes: Record<string, ExchangeBalance> | null;
145
+ /** Hyperliquid: the account abstraction mode the balance was read under; null elsewhere. */
146
+ accountMode: string | null;
147
+ /** Sub-reads that failed (Hyperliquid: one HIP-3 dex, "hip3:<dex>") — unknown, never zero. */
148
+ errors: Record<string, {
149
+ code: string;
150
+ message: string;
151
+ }>;
152
+ notes: string[];
153
+ }
154
+ export interface PublicPositionsOptions {
155
+ /** Lighter: one account of the address (sub-accounts); default every account. */
156
+ accountIndex?: number;
157
+ }
158
+ /** A position read by public address: the keyed row plus where it lives. */
159
+ export interface PublicPosition extends ExchangePosition {
160
+ /** Hyperliquid: the HIP-3 dex of the row; null = the native perp dex. */
161
+ dex?: string | null;
162
+ /** Lighter: the account (sub-account) index the row belongs to. */
163
+ accountIndex?: number;
164
+ }
165
+ export interface PublicPositionsResult {
166
+ positions: PublicPosition[];
167
+ /**
168
+ * Sub-reads that failed while the others succeeded (Hyperliquid: one HIP-3 dex), keyed by
169
+ * source ("hip3:xyz"). Their rows are unknown — never silently absent: callers surface them.
170
+ */
171
+ errors: Record<string, {
172
+ code: string;
173
+ message: string;
174
+ }>;
175
+ /** Human notes on an empty answer ("no Lighter account for this address"). */
176
+ notes: string[];
108
177
  }
109
178
  export interface VenueCredentials {
110
179
  /** Raw private key from --private-key or (legacy venues only) env. */
@@ -216,6 +285,24 @@ export declare function tryResolveVenueId(nameOrAlias: string): VenueId | null;
216
285
  export declare function getVenue(nameOrAlias: string): VenueDefinition;
217
286
  export declare function listVenues(): VenueDefinition[];
218
287
  export declare function listVenuesWithRole(role: VenueRole): VenueDefinition[];
288
+ /** Venues that read positions by public address (VenuePublicData.fetchPositionsByAddress). */
289
+ export declare function listPublicPositionVenues(): VenueDefinition[];
290
+ /** CAPABILITY_UNSUPPORTED for a venue without public-address position reads (registry-derived). */
291
+ export declare function publicPositionsUnsupported(def: VenueDefinition): PerpError;
292
+ /**
293
+ * Open positions of `address` on `venue`, keyless: unsupported venue → CAPABILITY_UNSUPPORTED,
294
+ * wrong address format → INVALID_PARAMS, both before any request.
295
+ */
296
+ export declare function fetchPositionsByAddress(venue: string, net: Network, address: string, opts?: PublicPositionsOptions): Promise<PublicPositionsResult>;
297
+ /** Venues that read balances by public address (VenuePublicData.fetchBalanceByAddress). */
298
+ export declare function listPublicBalanceVenues(): VenueDefinition[];
299
+ /** CAPABILITY_UNSUPPORTED for a venue without public-address balance reads (registry-derived). */
300
+ export declare function publicBalanceUnsupported(def: VenueDefinition): PerpError;
301
+ /**
302
+ * Balances of `address` on `venue`, keyless: unsupported venue → CAPABILITY_UNSUPPORTED, wrong
303
+ * address format → INVALID_PARAMS, both before any request.
304
+ */
305
+ export declare function fetchBalanceByAddress(venue: string, net: Network, address: string, opts?: PublicPositionsOptions): Promise<PublicBalanceResult>;
219
306
  export declare function networkConfig(def: VenueDefinition, net: Network): VenueNetworkConfig;
220
307
  /**
221
308
  * The uniform refusal for any account / trading / funds / agent / builder / bot action on a venue
@@ -248,6 +335,15 @@ export declare function settlementIntervalOf(ref: string): number;
248
335
  * CAPABILITY_UNSUPPORTED. Unknown venue refs throw UNKNOWN_VENUE.
249
336
  */
250
337
  export declare function assertFundingArbVenue(ref: string): void;
338
+ /**
339
+ * Market rows (`market list|prices|info`, MCP get_markets) state whether their venue's funding rate
340
+ * is verified — on every venue, true or false (VenueDefinition.fundingVerified), so a consumer
341
+ * never has to read a missing field as "verified". A venue ref ("hip3:xyz" → hyperliquid) is
342
+ * resolved; an unknown ref leaves the rows as they are.
343
+ */
344
+ export declare function withFundingVerified<T extends object>(venue: string, rows: T[]): Array<T & {
345
+ fundingVerified: boolean;
346
+ }> | T[];
251
347
  export declare function isLegacyVenue(def: VenueDefinition): def is VenueDefinition & {
252
348
  id: LegacyVenueId;
253
349
  };
@@ -9,6 +9,7 @@
9
9
  * `create`) so that public-data paths never pull in heavy SDKs (lighter WASM).
10
10
  */
11
11
  import { PerpError } from "../errors.js";
12
+ import { requireAddressOfKind } from "./address.js";
12
13
  import { isVenueId } from "./venue-types.js";
13
14
  import { optionsVenueWords } from "./options-venues.js";
14
15
  export const READ_ONLY_CREDENTIALS = {
@@ -72,6 +73,64 @@ export function listVenues() {
72
73
  export function listVenuesWithRole(role) {
73
74
  return listVenues().filter(v => v.roles.includes(role));
74
75
  }
76
+ /** Venues that read positions by public address (VenuePublicData.fetchPositionsByAddress). */
77
+ export function listPublicPositionVenues() {
78
+ return listVenues().filter(v => typeof v.publicData.fetchPositionsByAddress === "function" && v.publicData.addressKind !== undefined);
79
+ }
80
+ /** CAPABILITY_UNSUPPORTED for a venue without public-address position reads (registry-derived). */
81
+ export function publicPositionsUnsupported(def) {
82
+ const reason = !def.roles.includes("account")
83
+ ? `${def.displayName} is available for market data only (no account reads)`
84
+ : `${def.displayName} publishes positions only to the account itself (signed or API-key reads)`;
85
+ const supported = listPublicPositionVenues().map(v => v.id).join(", ");
86
+ return new PerpError("CAPABILITY_UNSUPPORTED", `${def.id} does not support publicPositions — ${reason}`, {
87
+ venue: def.id, feature: "publicPositions", reason,
88
+ remediation: `Public-address reads: ${supported}; for your own account: perp -e ${def.id} account positions`,
89
+ });
90
+ }
91
+ /**
92
+ * Open positions of `address` on `venue`, keyless: unsupported venue → CAPABILITY_UNSUPPORTED,
93
+ * wrong address format → INVALID_PARAMS, both before any request.
94
+ */
95
+ export async function fetchPositionsByAddress(venue, net, address, opts = {}) {
96
+ const def = getVenue(venue);
97
+ const pd = def.publicData;
98
+ if (pd.fetchPositionsByAddress === undefined || pd.addressKind === undefined)
99
+ throw publicPositionsUnsupported(def);
100
+ if (opts.accountIndex !== undefined && def.id !== "lighter" && def.id !== "lighter-rh") {
101
+ throw new PerpError("INVALID_PARAMS", `--account-index applies to Lighter only (${def.id} has one account per address)`, { exchange: def.id, remediation: "Drop --account-index" });
102
+ }
103
+ return pd.fetchPositionsByAddress(net, requireAddressOfKind(def.id, pd.addressKind, address), opts);
104
+ }
105
+ /** Venues that read balances by public address (VenuePublicData.fetchBalanceByAddress). */
106
+ export function listPublicBalanceVenues() {
107
+ return listVenues().filter(v => typeof v.publicData.fetchBalanceByAddress === "function" && v.publicData.addressKind !== undefined);
108
+ }
109
+ /** CAPABILITY_UNSUPPORTED for a venue without public-address balance reads (registry-derived). */
110
+ export function publicBalanceUnsupported(def) {
111
+ const reason = !def.roles.includes("account")
112
+ ? `${def.displayName} is available for market data only (no account reads)`
113
+ : `${def.displayName} publishes balances only to the account itself (signed or API-key reads)`;
114
+ const supported = listPublicBalanceVenues().map(v => v.id).join(", ");
115
+ return new PerpError("CAPABILITY_UNSUPPORTED", `${def.id} does not support publicBalance — ${reason}`, {
116
+ venue: def.id, feature: "publicBalance", reason,
117
+ remediation: `Public-address reads: ${supported}; for your own account: perp -e ${def.id} portfolio`,
118
+ });
119
+ }
120
+ /**
121
+ * Balances of `address` on `venue`, keyless: unsupported venue → CAPABILITY_UNSUPPORTED, wrong
122
+ * address format → INVALID_PARAMS, both before any request.
123
+ */
124
+ export async function fetchBalanceByAddress(venue, net, address, opts = {}) {
125
+ const def = getVenue(venue);
126
+ const pd = def.publicData;
127
+ if (pd.fetchBalanceByAddress === undefined || pd.addressKind === undefined)
128
+ throw publicBalanceUnsupported(def);
129
+ if (opts.accountIndex !== undefined && def.id !== "lighter" && def.id !== "lighter-rh") {
130
+ throw new PerpError("INVALID_PARAMS", `--account-index applies to Lighter only (${def.id} has one account per address)`, { exchange: def.id, remediation: "Drop --account-index" });
131
+ }
132
+ return pd.fetchBalanceByAddress(net, requireAddressOfKind(def.id, pd.addressKind, address), opts);
133
+ }
75
134
  export function networkConfig(def, net) {
76
135
  const cfg = def.networks[net];
77
136
  if (!cfg) {
@@ -176,6 +235,19 @@ export function assertFundingArbVenue(ref) {
176
235
  remediation: `Use venues with verified funding (${listVenues().filter(v => v.fundingVerified && v.roles.includes("trading")).map(v => v.id).join(", ")}). ${def.displayName} rates can still be viewed: perp arb scan --rates --include-unverified`,
177
236
  });
178
237
  }
238
+ /**
239
+ * Market rows (`market list|prices|info`, MCP get_markets) state whether their venue's funding rate
240
+ * is verified — on every venue, true or false (VenueDefinition.fundingVerified), so a consumer
241
+ * never has to read a missing field as "verified". A venue ref ("hip3:xyz" → hyperliquid) is
242
+ * resolved; an unknown ref leaves the rows as they are.
243
+ */
244
+ export function withFundingVerified(venue, rows) {
245
+ const id = tryResolveVenueId(venue.startsWith("hip3:") ? "hyperliquid" : venue);
246
+ const def = id === null ? undefined : _venues.get(id);
247
+ if (def === undefined)
248
+ return rows;
249
+ return rows.map(r => ({ ...r, fundingVerified: def.fundingVerified }));
250
+ }
179
251
  export function isLegacyVenue(def) {
180
252
  return def.id === "pacifica" || def.id === "hyperliquid" || def.id === "lighter" || def.id === "aster";
181
253
  }
@@ -26,6 +26,15 @@ const publicData = {
26
26
  async fetchMarkets(net) {
27
27
  return (await import("../../funding/legacy-public.js")).legacyPublicData("hyperliquid").fetchMarkets(net);
28
28
  },
29
+ // Keyless positions of any account by public address (account positions --address).
30
+ addressKind: "evm",
31
+ async fetchPositionsByAddress(net, address) {
32
+ return (await import("../public-positions.js")).hyperliquidPositionsByAddress(net, address);
33
+ },
34
+ // Keyless balances of any account by public address (account balance --address).
35
+ async fetchBalanceByAddress(net, address) {
36
+ return (await import("../public-balances.js")).hyperliquidBalanceByAddress(net, address);
37
+ },
29
38
  };
30
39
  export const hyperliquidVenue = {
31
40
  id: "hyperliquid",
@@ -9,8 +9,17 @@ const publicData = {
9
9
  async fetchFundingRates(net) {
10
10
  return (await import("../lighter-public.js")).lighterPublicData("lighter-rh").fetchFundingRates(net);
11
11
  },
12
- async fetchMarkets(net) {
13
- return (await import("../lighter-public.js")).lighterPublicData("lighter-rh").fetchMarkets(net);
12
+ async fetchMarkets(net, opts) {
13
+ return (await import("../lighter-public.js")).lighterPublicData("lighter-rh").fetchMarkets(net, opts);
14
+ },
15
+ // Keyless positions of any account by public address (account positions --address).
16
+ addressKind: "evm",
17
+ async fetchPositionsByAddress(net, address, opts) {
18
+ return (await import("../public-positions.js")).lighterPositionsByAddress("lighter-rh", net, address, opts);
19
+ },
20
+ // Keyless balances of any account by public address (account balance --address).
21
+ async fetchBalanceByAddress(net, address, opts) {
22
+ return (await import("../public-balances.js")).lighterBalanceByAddress("lighter-rh", net, address, opts);
14
23
  },
15
24
  };
16
25
  export const lighterRhVenue = {
@@ -23,7 +32,8 @@ export const lighterRhVenue = {
23
32
  secretStorage: { kind: "perp-keystore", dir: "lighter-rh-agents" },
24
33
  networks: LIGHTER_RH_NETWORKS,
25
34
  // "Lighter RH is the same API/engine as Lighter" → Lighter's basis/interval (live-data check pending).
26
- fundingRateBasis: { kind: "perHours", hours: 8 },
35
+ // The market rows' fundingRate is the settled hourly rate (WS market_stats, lighter-public.ts).
36
+ fundingRateBasis: { kind: "perHours", hours: 1 },
27
37
  settlementIntervalHours: 1,
28
38
  fundingVerified: true,
29
39
  roles: ["marketData", "account", "trading"],
@@ -8,8 +8,17 @@ const publicData = {
8
8
  async fetchFundingRates(net) {
9
9
  return (await import("../lighter-public.js")).lighterPublicData("lighter").fetchFundingRates(net);
10
10
  },
11
- async fetchMarkets(net) {
12
- return (await import("../lighter-public.js")).lighterPublicData("lighter").fetchMarkets(net);
11
+ async fetchMarkets(net, opts) {
12
+ return (await import("../lighter-public.js")).lighterPublicData("lighter").fetchMarkets(net, opts);
13
+ },
14
+ // Keyless positions of any account by public address (account positions --address).
15
+ addressKind: "evm",
16
+ async fetchPositionsByAddress(net, address, opts) {
17
+ return (await import("../public-positions.js")).lighterPositionsByAddress("lighter", net, address, opts);
18
+ },
19
+ // Keyless balances of any account by public address (account balance --address).
20
+ async fetchBalanceByAddress(net, address, opts) {
21
+ return (await import("../public-balances.js")).lighterBalanceByAddress("lighter", net, address, opts);
13
22
  },
14
23
  };
15
24
  export const lighterVenue = {
@@ -21,7 +30,8 @@ export const lighterVenue = {
21
30
  agentKeyKind: "lighter-l2",
22
31
  secretStorage: { kind: "perp-keystore", dir: "lighter-agents" },
23
32
  networks: LIGHTER_NETWORKS,
24
- fundingRateBasis: { kind: "perHours", hours: 8 },
33
+ // The market rows' fundingRate is the settled hourly rate (WS market_stats, lighter-public.ts).
34
+ fundingRateBasis: { kind: "perHours", hours: 1 },
25
35
  settlementIntervalHours: 1,
26
36
  fundingVerified: true,
27
37
  roles: ["marketData", "account", "trading"],
@@ -10,6 +10,15 @@ const publicData = {
10
10
  async fetchMarkets(net) {
11
11
  return (await import("../../funding/legacy-public.js")).legacyPublicData("pacifica").fetchMarkets(net);
12
12
  },
13
+ // Keyless positions of any account by public address (account positions --address).
14
+ addressKind: "solana",
15
+ async fetchPositionsByAddress(net, address) {
16
+ return (await import("../public-positions.js")).pacificaPositionsByAddress(net, address);
17
+ },
18
+ // Keyless balances of any account by public address (account balance --address).
19
+ async fetchBalanceByAddress(net, address) {
20
+ return (await import("../public-balances.js")).pacificaBalanceByAddress(net, address);
21
+ },
13
22
  };
14
23
  /** Pacifica `client_order_id` format: "Full UUID string" (docs.pacifica.fi rest-api orders/create-limit-order). */
15
24
  export const PACIFICA_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -35,6 +35,21 @@ const publicData = {
35
35
  async fetchMarkets(net) {
36
36
  return (await import("../risex/public.js")).risexPublicData.fetchMarkets(net);
37
37
  },
38
+ // Keyless positions of any account by public address (account positions --address): the
39
+ // adapter's own public-by-address read (/v1/portfolio/details), no signer.
40
+ addressKind: "evm",
41
+ async fetchPositionsByAddress(net, address) {
42
+ const { RisexAdapter } = await import("../risex/adapter.js");
43
+ const adapter = new RisexAdapter(risexVenue, net, { account: address, signer: null, meta: null, noSignerReason: null });
44
+ return { positions: await adapter.getPositions(), errors: {}, notes: [] };
45
+ },
46
+ // Keyless balance (account balance --address): the same signer-less adapter's getBalance. RISEx
47
+ // has no spot market: spot null.
48
+ async fetchBalanceByAddress(net, address) {
49
+ const { RisexAdapter } = await import("../risex/adapter.js");
50
+ const adapter = new RisexAdapter(risexVenue, net, { account: address, signer: null, meta: null, noSignerReason: null });
51
+ return { balance: await adapter.getBalance(), spot: null, accounts: null, dexes: null, accountMode: null, errors: {}, notes: [] };
52
+ },
38
53
  };
39
54
  export const risexVenue = {
40
55
  id: "risex",
@@ -19,13 +19,15 @@
19
19
  * pacifica: crypto-centric; PACIFICA_NON_CRYPTO lists every non-crypto perp
20
20
  * listed on 2026-09-30.
21
21
  * aster: exchangeInfo `underlyingSubType` (STOCK / ETF / Commodities / pre-launch).
22
- * AMBIGUOUS tickers are excluded with a warning so they cannot produce a false
23
- * cross-venue match (SPX = the SPX6900 token, whose ticker equals the S&P 500
24
- * canonical key).
22
+ * Ambiguous tickers (UNCLASSIFIED_TICKERS, src/ws/unclassified.ts — SPX = the SPX6900
23
+ * token, whose ticker equals the S&P 500 canonical key) are listed as "unclassified"
24
+ * (canonical null) so they cannot produce a false cross-venue match.
25
25
  *
26
26
  * Fields a venue does not publish on this path are null (HL has no fixed tick
27
27
  * size; Aster leverage brackets need a signed call; Aster OI is per-symbol only).
28
28
  */
29
29
  import type { VenuePublicData } from "../exchanges/venue.js";
30
30
  export type LegacyPublicVenue = "pacifica" | "hyperliquid" | "aster";
31
+ /** One public JSON read with the venue error mapping (rate limits, HTTP status, invalid JSON). */
32
+ export declare function legacyPublicJson(venue: LegacyPublicVenue, url: string, label: string, init?: RequestInit): Promise<unknown>;
31
33
  export declare function legacyPublicData(venue: LegacyPublicVenue): VenuePublicData;
@@ -19,24 +19,26 @@
19
19
  * pacifica: crypto-centric; PACIFICA_NON_CRYPTO lists every non-crypto perp
20
20
  * listed on 2026-09-30.
21
21
  * aster: exchangeInfo `underlyingSubType` (STOCK / ETF / Commodities / pre-launch).
22
- * AMBIGUOUS tickers are excluded with a warning so they cannot produce a false
23
- * cross-venue match (SPX = the SPX6900 token, whose ticker equals the S&P 500
24
- * canonical key).
22
+ * Ambiguous tickers (UNCLASSIFIED_TICKERS, src/ws/unclassified.ts — SPX = the SPX6900
23
+ * token, whose ticker equals the S&P 500 canonical key) are listed as "unclassified"
24
+ * (canonical null) so they cannot produce a false cross-venue match.
25
25
  *
26
26
  * Fields a venue does not publish on this path are null (HL has no fixed tick
27
27
  * size; Aster leverage brackets need a signed call; Aster OI is per-symbol only).
28
28
  */
29
29
  import { HL_MIN_ORDER_VALUE_USD } from "../constants.js";
30
30
  import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
31
+ import { asterCliSymbol, asterMatchable, isAsterListedPerpetual } from "../ws/aster-market.js";
32
+ import { isUnclassifiedTicker } from "../ws/unclassified.js";
31
33
  import { PerpError } from "../errors.js";
32
34
  import { venueRateString } from "../utils/numeric.js";
33
35
  import { assertOk } from "../api/public/_http.js";
34
36
  import { legacyPublicBaseUrl } from "../api/public/urls.js";
35
37
  import { canonicalFor } from "../exchanges/canonical-symbols.js";
36
38
  import { fundingBasisOf, settlementIntervalOf } from "../exchanges/venue.js";
39
+ import { nextTopOfHourMs } from "../exchanges/venue-types.js";
37
40
  // ── Classification ──
38
- const AMBIGUOUS = new Set(["SPX"]);
39
- /** Pacifica non-crypto perps (live /info 2026-09-30). BP / CHIP: ticker collides with a listed equity/ETF. */
41
+ /** Pacifica non-crypto perps (live /info 2026-09-30). Ambiguous BP / CHIP / SPX: UNCLASSIFIED_TICKERS. */
40
42
  const PACIFICA_NON_CRYPTO = {
41
43
  NVDA: "equity", TSLA: "equity", GOOGL: "equity", PLTR: "equity", HOOD: "equity", CRCL: "equity",
42
44
  SPCX: "equity", SKHYNIX: "equity", SAMSUNG: "equity", MSTR: "equity", MU: "equity", SNDK: "equity",
@@ -47,19 +49,20 @@ const PACIFICA_NON_CRYPTO = {
47
49
  USDJPY: "fx", EURUSD: "fx",
48
50
  SP500: "index",
49
51
  };
50
- const PACIFICA_AMBIGUOUS = new Set(["BP", "CHIP"]);
52
+ // Ambiguous tickers are listed as "unclassified" (canonical null: never matched across venues),
53
+ // so every listing path (CLI, MCP, public directory) shows the same market set.
51
54
  function classifyPacifica(symbol) {
52
55
  const up = symbol.toUpperCase();
53
- if (AMBIGUOUS.has(up) || PACIFICA_AMBIGUOUS.has(up))
54
- return null;
56
+ if (isUnclassifiedTicker("pacifica", up))
57
+ return "unclassified";
55
58
  return PACIFICA_NON_CRYPTO[up] ?? "crypto"; // crypto-centric venue: non-crypto is listed explicitly
56
59
  }
57
60
  function classifyHyperliquid(symbol) {
58
- return AMBIGUOUS.has(symbol.toUpperCase()) ? null : "crypto";
61
+ return isUnclassifiedTicker("hyperliquid", symbol) ? "unclassified" : "crypto";
59
62
  }
60
63
  function classifyAster(symbol, subTypes) {
61
- if (AMBIGUOUS.has(symbol.toUpperCase()))
62
- return null;
64
+ if (isUnclassifiedTicker("aster", symbol))
65
+ return "unclassified";
63
66
  if (subTypes.includes("pre-launch"))
64
67
  return "preIpo";
65
68
  if (subTypes.includes("ETF"))
@@ -76,6 +79,10 @@ function warnExcluded(venue, what, symbols) {
76
79
  }
77
80
  }
78
81
  // ── HTTP ──
82
+ /** One public JSON read with the venue error mapping (rate limits, HTTP status, invalid JSON). */
83
+ export async function legacyPublicJson(venue, url, label, init) {
84
+ return getJson(venue, url, label, init);
85
+ }
79
86
  async function getJson(venue, url, label, init) {
80
87
  const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) });
81
88
  await assertOk(res, `${venue} ${label}`);
@@ -125,7 +132,6 @@ async function pacificaRows(net) {
125
132
  const basis = fundingBasisOf("pacifica");
126
133
  const settlement = settlementIntervalOf("pacifica");
127
134
  const out = [];
128
- const unclassified = [];
129
135
  const unpriced = [];
130
136
  for (const m of info) {
131
137
  // /info lists spot markets too (e.g. SOL-USDC): perps only. A row without the field cannot be classified.
@@ -136,10 +142,6 @@ async function pacificaRows(net) {
136
142
  continue;
137
143
  const symbol = String(m.symbol ?? "");
138
144
  const assetClass = classifyPacifica(symbol);
139
- if (assetClass === null) {
140
- unclassified.push(symbol);
141
- continue;
142
- }
143
145
  const p = priceBySymbol.get(symbol);
144
146
  const mark = p ? positiveNumStr(p.mark) : null;
145
147
  if (!p || mark === null) {
@@ -158,7 +160,9 @@ async function pacificaRows(net) {
158
160
  quoteAsset: null,
159
161
  markPrice: mark,
160
162
  indexPrice: positiveNumStr(p.oracle),
161
- fundingRate: rateStr(p.next_funding),
163
+ // Last settled hourly rate (WS prices `funding`, the CLI row's fundingRate); next_funding
164
+ // is the predicted one.
165
+ fundingRate: rateStr(p.funding),
162
166
  fundingRateBasis: basis,
163
167
  settlementIntervalHours: settlement,
164
168
  openInterest: numStr(p.open_interest),
@@ -172,10 +176,9 @@ async function pacificaRows(net) {
172
176
  session: null,
173
177
  tradable: true,
174
178
  },
175
- nextFundingTime: null, // Pacifica publishes no next-funding timestamp
179
+ nextFundingTime: nextTopOfHourMs(), // not published; docs: settled at the end of each 1 h interval (on the hour)
176
180
  });
177
181
  }
178
- warnExcluded("pacifica", "unclassified/ambiguous market(s)", unclassified);
179
182
  warnExcluded("pacifica", "market(s) without a mark price", unpriced);
180
183
  return out;
181
184
  }
@@ -198,7 +201,6 @@ async function hyperliquidRows(net) {
198
201
  const basis = fundingBasisOf("hyperliquid");
199
202
  const settlement = settlementIntervalOf("hyperliquid");
200
203
  const out = [];
201
- const unclassified = [];
202
204
  const unpriced = [];
203
205
  universe.forEach((a, i) => {
204
206
  // Delisted assets stay in meta (isDelisted: true) with zeroed funding / volume / OI
@@ -208,10 +210,6 @@ async function hyperliquidRows(net) {
208
210
  const ctx = ctxs[i];
209
211
  const symbol = String(a.name ?? "");
210
212
  const assetClass = classifyHyperliquid(symbol);
211
- if (assetClass === null) {
212
- unclassified.push(symbol);
213
- return;
214
- }
215
213
  const mark = positiveNumStr(ctx.markPx);
216
214
  if (mark === null) {
217
215
  unpriced.push(symbol);
@@ -244,10 +242,9 @@ async function hyperliquidRows(net) {
244
242
  session: null,
245
243
  tradable: true, // delisted assets are skipped above
246
244
  },
247
- nextFundingTime: null, // not published in metaAndAssetCtxs
245
+ nextFundingTime: nextTopOfHourMs(), // not in metaAndAssetCtxs; docs: paid every hour (on the hour)
248
246
  });
249
247
  });
250
- warnExcluded("hyperliquid", "unclassified/ambiguous market(s)", unclassified);
251
248
  warnExcluded("hyperliquid", "market(s) without a mark price", unpriced);
252
249
  return out;
253
250
  }
@@ -276,20 +273,20 @@ async function asterRows(net) {
276
273
  intervalBySymbol.set(String(f.symbol), h);
277
274
  }
278
275
  const out = [];
279
- const unclassified = [];
280
276
  const unpriced = [];
281
277
  const noInterval = [];
282
278
  for (const s of symbols) {
283
- if (s.contractType !== "PERPETUAL" || s.quoteAsset !== "USDT")
279
+ // Shared eligibility (SETTLING / PENDING_TRADING rows were listed with tradable false); every
280
+ // quote, like the CLI adapter and the WS directory.
281
+ if (!isAsterListedPerpetual(s))
284
282
  continue;
285
283
  const venueSymbol = String(s.symbol);
286
- const symbol = String(s.baseAsset);
284
+ const symbol = asterCliSymbol({ symbol: venueSymbol, baseAsset: String(s.baseAsset), quoteAsset: s.quoteAsset });
285
+ const base = String(s.baseAsset);
287
286
  const subTypes = Array.isArray(s.underlyingSubType) ? s.underlyingSubType.map(String) : [];
288
- const assetClass = classifyAster(symbol, subTypes);
289
- if (assetClass === null) {
290
- unclassified.push(venueSymbol);
291
- continue;
292
- }
287
+ const assetClass = classifyAster(base, subTypes);
288
+ // A second listing of a base (BTCUSD1 beside BTCUSDT) or a non-USD quote (U): canonical null.
289
+ const matchable = asterMatchable(s);
293
290
  const p = premiumBySymbol.get(venueSymbol);
294
291
  const mark = p ? positiveNumStr(p.markPrice) : null;
295
292
  if (!p || mark === null) {
@@ -311,10 +308,10 @@ async function asterRows(net) {
311
308
  venue: "aster",
312
309
  venueSymbol,
313
310
  symbol,
314
- canonical: canonicalFor("aster", symbol, assetClass),
311
+ canonical: matchable ? canonicalFor("aster", base, assetClass) : null,
315
312
  assetClass,
316
313
  settle: s.marginAsset,
317
- quoteAsset: s.quoteAsset,
314
+ quoteAsset: typeof s.quoteAsset === "string" && s.quoteAsset !== "" ? s.quoteAsset : null,
318
315
  markPrice: mark,
319
316
  indexPrice: positiveNumStr(p.indexPrice),
320
317
  fundingRate: rateStr(p.lastFundingRate),
@@ -334,7 +331,6 @@ async function asterRows(net) {
334
331
  nextFundingTime: Number.isFinite(next) && next > 0 ? next : null,
335
332
  });
336
333
  }
337
- warnExcluded("aster", "unclassified/ambiguous market(s)", unclassified);
338
334
  warnExcluded("aster", "market(s) without a mark price", unpriced);
339
335
  warnExcluded("aster", "market(s) missing from fundingInfo", noInterval);
340
336
  return out;