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.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- 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
|
|
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
|
-
|
|
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
|
};
|
package/dist/exchanges/venue.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* canonical
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* canonical
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
54
|
-
return
|
|
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
|
|
61
|
+
return isUnclassifiedTicker("hyperliquid", symbol) ? "unclassified" : "crypto";
|
|
59
62
|
}
|
|
60
63
|
function classifyAster(symbol, subTypes) {
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
289
|
-
|
|
290
|
-
|
|
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",
|
|
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;
|