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
package/dist/funding/rates.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type HistoricalAverages } from "./history.js";
|
|
14
14
|
import { type VenueDefinition } from "../exchanges/venue.js";
|
|
15
|
+
import type { FundingRateKind } from "../exchanges/interface.js";
|
|
15
16
|
import { type FundingRateBasis, type Network, type VenueId } from "../exchanges/venue-types.js";
|
|
16
17
|
export interface ExchangeFundingRate {
|
|
17
18
|
exchange: VenueId;
|
|
@@ -30,27 +31,61 @@ export interface ExchangeFundingRate {
|
|
|
30
31
|
fundingVerified: boolean;
|
|
31
32
|
/** How the row was read: "unofficial" = an undocumented venue API (VenueDefinition.apiStatus) — may change without notice. */
|
|
32
33
|
apiStatus: "official" | "unofficial";
|
|
34
|
+
/**
|
|
35
|
+
* Which rate `fundingRate` is (the venue market row's rate — arb compares those, never a
|
|
36
|
+
* predicted next-period rate unless the venue only publishes that): "lastSettled" | "accruing" |
|
|
37
|
+
* "predicted", null = not verified (fundingRateKindOf).
|
|
38
|
+
*/
|
|
39
|
+
rateKind: FundingRateKind | null;
|
|
33
40
|
}
|
|
34
41
|
export interface SymbolFundingComparison {
|
|
35
42
|
/** Canonical symbol shared by every row. */
|
|
36
43
|
symbol: string;
|
|
37
44
|
rates: ExchangeFundingRate[];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Ranking is over VERIFIED rows only (VenueDefinition.fundingVerified): an unverified row
|
|
47
|
+
* (`--include-unverified`) is shown in `rates` but is never the long or short leg nor part of
|
|
48
|
+
* the spread. Fewer than two verified rows → no ranked pair: longExchange / shortExchange /
|
|
49
|
+
* maxSpreadAnnual / estHourlyIncomeUsd are null (a display-only row).
|
|
50
|
+
*/
|
|
51
|
+
maxSpreadAnnual: number | null;
|
|
52
|
+
longExchange: string | null;
|
|
53
|
+
shortExchange: string | null;
|
|
41
54
|
bestMarkPrice: number;
|
|
42
|
-
estHourlyIncomeUsd: number;
|
|
55
|
+
estHourlyIncomeUsd: number | null;
|
|
43
56
|
/**
|
|
44
57
|
* false = a leg of the long/short pair sits on a venue without the trading role (RISEx while its
|
|
45
58
|
* trading is gated in this release): the spread is data only — `arb exec` refuses it.
|
|
46
59
|
*/
|
|
47
60
|
executable: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* true = the row's rates are of different kinds (e.g. Lighter lastSettled vs Hyperliquid
|
|
63
|
+
* accruing) — the perp-ws comparator's `mixedKinds`: the spread compares unlike quantities.
|
|
64
|
+
*/
|
|
65
|
+
mixedKinds: boolean;
|
|
48
66
|
/** Venues of the long/short pair that perp-cli cannot trade (read-only); [] when executable. */
|
|
49
67
|
readOnlyLegs: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Venues of the long/short pair whose funding is unverified (VenueDefinition.fundingVerified
|
|
70
|
+
* false, e.g. QFEX — shown only with --include-unverified, display only); such a pair is never
|
|
71
|
+
* executable. [] when both legs are verified.
|
|
72
|
+
*/
|
|
73
|
+
unverifiedLegs: string[];
|
|
50
74
|
}
|
|
75
|
+
/** A comparison with a ranked pair (≥ 2 verified rows): long / short / spread / income are set. */
|
|
76
|
+
export type RankedFundingComparison = SymbolFundingComparison & {
|
|
77
|
+
maxSpreadAnnual: number;
|
|
78
|
+
longExchange: string;
|
|
79
|
+
shortExchange: string;
|
|
80
|
+
estHourlyIncomeUsd: number;
|
|
81
|
+
};
|
|
82
|
+
export declare function isRankedComparison(c: SymbolFundingComparison): c is RankedFundingComparison;
|
|
51
83
|
export interface FundingRateSnapshot {
|
|
52
84
|
timestamp: string;
|
|
53
85
|
symbols: SymbolFundingComparison[];
|
|
86
|
+
/** The minSpread applied (annualized %) and how many ranked pairs it hid (spread below it). */
|
|
87
|
+
minSpread: number;
|
|
88
|
+
belowMinSpread: number;
|
|
54
89
|
exchangeStatus: Record<string, "ok" | "error">;
|
|
55
90
|
/** Error message per venue whose status is "error". */
|
|
56
91
|
exchangeErrors: Record<string, string>;
|
|
@@ -68,6 +103,16 @@ export declare function fetchVenuesRates(venues: VenueDefinition[], net: Network
|
|
|
68
103
|
exchangeStatus: Record<string, "ok" | "error">;
|
|
69
104
|
exchangeErrors: Record<string, string>;
|
|
70
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Same canonical ticker on two venues is not proof of the same asset (e.g. two
|
|
108
|
+
* unrelated tokens both listed as "PRL"). Within a canonical group, drop any
|
|
109
|
+
* venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
|
|
110
|
+
* group median, with a one-time stderr warning per (symbol, venue).
|
|
111
|
+
*/
|
|
112
|
+
export declare function dropPriceOutliers<T extends {
|
|
113
|
+
exchange: VenueId;
|
|
114
|
+
markPrice: number;
|
|
115
|
+
}>(canonical: string, list: T[], tag?: string): T[];
|
|
71
116
|
export interface VenuePrice {
|
|
72
117
|
exchange: VenueId;
|
|
73
118
|
/** Venue display symbol. */
|
|
@@ -94,10 +139,14 @@ export declare function fetchCanonicalPriceGroups(venues: VenueDefinition[], opt
|
|
|
94
139
|
network?: Network;
|
|
95
140
|
tag?: string;
|
|
96
141
|
}): Promise<PriceGroupSnapshot>;
|
|
97
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
|
|
144
|
+
* verified funding (an unverified-funding venue is display only — never ranked for trading).
|
|
145
|
+
*/
|
|
98
146
|
export declare function pairExecutability(longExchange: string, shortExchange: string): {
|
|
99
147
|
executable: boolean;
|
|
100
148
|
readOnlyLegs: string[];
|
|
149
|
+
unverifiedLegs: string[];
|
|
101
150
|
};
|
|
102
151
|
/**
|
|
103
152
|
* Fetch funding rates from every market-data venue in parallel, normalize, and compare.
|
package/dist/funding/rates.js
CHANGED
|
@@ -14,10 +14,15 @@ import { annualizeHourlyRate, estimateHourlyFundingFromHourly } from "./normaliz
|
|
|
14
14
|
import { getHistoricalAverages } from "./history.js";
|
|
15
15
|
import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
16
16
|
import { getVenue, listVenuesWithRole, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
17
|
-
import { rateToHourly } from "../exchanges/venue-types.js";
|
|
17
|
+
import { fundingRateKindOf, rateToHourly } from "../exchanges/venue-types.js";
|
|
18
18
|
import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
|
|
19
19
|
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
20
20
|
import { PerpError } from "../errors.js";
|
|
21
|
+
import { canonicalQuerySymbol } from "../exchanges/canonical-symbols.js";
|
|
22
|
+
export function isRankedComparison(c) {
|
|
23
|
+
// estHourlyIncomeUsd is set together with the pair (null exactly when the pair is).
|
|
24
|
+
return c.longExchange !== null && c.shortExchange !== null && c.maxSpreadAnnual !== null;
|
|
25
|
+
}
|
|
21
26
|
// ── Default top symbols to track ──
|
|
22
27
|
export const TOP_SYMBOLS = [
|
|
23
28
|
"BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
|
|
@@ -55,13 +60,17 @@ async function fetchVenueRates(def, net) {
|
|
|
55
60
|
for (const r of rows) {
|
|
56
61
|
if (r.canonical === null)
|
|
57
62
|
continue; // not matchable across venues
|
|
63
|
+
// Outside its funding hours (QFEX market_hours) a market has no rate: never ranked, snapshotted
|
|
64
|
+
// (saveFundingSnapshot) or averaged as a 0.
|
|
65
|
+
if (r.rate === null || r.marketClosed === true)
|
|
66
|
+
continue;
|
|
58
67
|
const markRaw = r.markPrice !== null ? r.markPrice : markBySymbol?.get(r.venueSymbol);
|
|
59
68
|
const markPrice = markRaw === undefined ? NaN : Number(markRaw);
|
|
60
69
|
if (!Number.isFinite(markPrice) || markPrice <= 0) {
|
|
61
70
|
unpriced.push(r.symbol);
|
|
62
71
|
continue;
|
|
63
72
|
}
|
|
64
|
-
out.push(toExchangeRate(r, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
|
|
73
|
+
out.push(toExchangeRate({ ...r, rate: r.rate }, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
|
|
65
74
|
}
|
|
66
75
|
if (unpriced.length > 0) {
|
|
67
76
|
console.warn(`[${def.id}] excluded ${unpriced.length} funding row(s) without a mark price: ${unpriced.join(", ")}`);
|
|
@@ -87,6 +96,7 @@ function toExchangeRate(r, canonical, markPrice, fundingVerified, apiStatus) {
|
|
|
87
96
|
historicalAvg: null,
|
|
88
97
|
fundingVerified,
|
|
89
98
|
apiStatus,
|
|
99
|
+
rateKind: fundingRateKindOf(r.venue, r.rate),
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
/**
|
|
@@ -122,7 +132,7 @@ const _warnedPriceMismatch = new Set();
|
|
|
122
132
|
* venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
|
|
123
133
|
* group median, with a one-time stderr warning per (symbol, venue).
|
|
124
134
|
*/
|
|
125
|
-
function dropPriceOutliers(canonical, list, tag = "funding") {
|
|
135
|
+
export function dropPriceOutliers(canonical, list, tag = "funding") {
|
|
126
136
|
if (list.length < 2)
|
|
127
137
|
return list;
|
|
128
138
|
const sorted = list.map(r => r.markPrice).sort((x, y) => x - y);
|
|
@@ -173,7 +183,7 @@ function groupByCanonical(rates, symbols, tag = "funding") {
|
|
|
173
183
|
export async function fetchCanonicalPriceGroups(venues, opts) {
|
|
174
184
|
ensureVenuesRegistered();
|
|
175
185
|
const net = opts?.network ?? sharedApiNetwork();
|
|
176
|
-
const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net)));
|
|
186
|
+
const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net, { funding: false })));
|
|
177
187
|
const exchangeStatus = {};
|
|
178
188
|
const exchangeErrors = {};
|
|
179
189
|
const prices = [];
|
|
@@ -196,10 +206,15 @@ export async function fetchCanonicalPriceGroups(venues, opts) {
|
|
|
196
206
|
});
|
|
197
207
|
return { groups: groupByCanonical(prices, opts?.symbols, opts?.tag ?? "prices"), exchangeStatus, exchangeErrors };
|
|
198
208
|
}
|
|
199
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
|
|
211
|
+
* verified funding (an unverified-funding venue is display only — never ranked for trading).
|
|
212
|
+
*/
|
|
200
213
|
export function pairExecutability(longExchange, shortExchange) {
|
|
201
|
-
const
|
|
202
|
-
|
|
214
|
+
const legs = [...new Set([longExchange, shortExchange])];
|
|
215
|
+
const readOnlyLegs = legs.filter(v => !getVenue(v).roles.includes("trading"));
|
|
216
|
+
const unverifiedLegs = legs.filter(v => !getVenue(v).fundingVerified);
|
|
217
|
+
return { executable: readOnlyLegs.length === 0 && unverifiedLegs.length === 0, readOnlyLegs, unverifiedLegs };
|
|
203
218
|
}
|
|
204
219
|
function bestMarkPrice(rates) {
|
|
205
220
|
const rank = (id) => MARK_PRICE_PREFERENCE.indexOf(id);
|
|
@@ -226,14 +241,28 @@ export async function fetchAllFundingRates(opts) {
|
|
|
226
241
|
}
|
|
227
242
|
const comparisons = [];
|
|
228
243
|
const minSpread = opts?.minSpread ?? 0;
|
|
244
|
+
let belowMinSpread = 0;
|
|
229
245
|
for (const [symbol, rates] of rateMap) {
|
|
230
246
|
// Need at least 2 exchanges to compare
|
|
231
247
|
if (rates.length < 2)
|
|
232
248
|
continue;
|
|
233
249
|
// Sort by hourly rate (ascending)
|
|
234
250
|
rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
// The long / short legs and the spread come from verified rows only: an unverified rate is
|
|
252
|
+
// display data (--include-unverified), never ranked. < 2 verified rows → a display-only row.
|
|
253
|
+
const ranked = rates.filter(r => r.fundingVerified);
|
|
254
|
+
if (ranked.length < 2) {
|
|
255
|
+
const unverifiedLegs = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
|
|
256
|
+
comparisons.push({
|
|
257
|
+
symbol, rates, maxSpreadAnnual: null, longExchange: null, shortExchange: null,
|
|
258
|
+
bestMarkPrice: bestMarkPrice(rates), estHourlyIncomeUsd: null,
|
|
259
|
+
executable: false, readOnlyLegs: [], unverifiedLegs,
|
|
260
|
+
mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const lowest = ranked[0];
|
|
265
|
+
const highest = ranked[ranked.length - 1];
|
|
237
266
|
// The median filter alone lets a 2-venue group through at up to 2 × MAX_PRICE_DEVIATION_PCT
|
|
238
267
|
// apart (each side deviates half the gap): the CHOSEN pair must itself agree on price.
|
|
239
268
|
const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
|
|
@@ -246,8 +275,10 @@ export async function fetchAllFundingRates(opts) {
|
|
|
246
275
|
continue;
|
|
247
276
|
}
|
|
248
277
|
const maxSpreadAnnual = annualizeHourlyRate(highest.hourlyRate - lowest.hourlyRate);
|
|
249
|
-
if (maxSpreadAnnual < minSpread)
|
|
278
|
+
if (maxSpreadAnnual < minSpread) {
|
|
279
|
+
belowMinSpread++;
|
|
250
280
|
continue;
|
|
281
|
+
}
|
|
251
282
|
// Estimate hourly income for $1000 notional arb
|
|
252
283
|
const notional = 1000;
|
|
253
284
|
const longIncome = estimateHourlyFundingFromHourly(lowest.hourlyRate, notional, "long");
|
|
@@ -262,13 +293,16 @@ export async function fetchAllFundingRates(opts) {
|
|
|
262
293
|
bestMarkPrice: bestMarkPrice(rates),
|
|
263
294
|
estHourlyIncomeUsd,
|
|
264
295
|
...pairExecutability(lowest.exchange, highest.exchange),
|
|
296
|
+
mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
|
|
265
297
|
});
|
|
266
298
|
}
|
|
267
|
-
// Sort by spread descending
|
|
268
|
-
comparisons.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual);
|
|
299
|
+
// Sort by spread descending; display-only rows (no ranked pair) last
|
|
300
|
+
comparisons.sort((a, b) => (b.maxSpreadAnnual ?? -Infinity) - (a.maxSpreadAnnual ?? -Infinity));
|
|
269
301
|
return {
|
|
270
302
|
timestamp: new Date().toISOString(),
|
|
271
303
|
symbols: comparisons,
|
|
304
|
+
minSpread,
|
|
305
|
+
belowMinSpread,
|
|
272
306
|
exchangeStatus,
|
|
273
307
|
exchangeErrors,
|
|
274
308
|
};
|
|
@@ -277,7 +311,9 @@ export async function fetchAllFundingRates(opts) {
|
|
|
277
311
|
* Fetch rates for a single (canonical) symbol across all exchanges.
|
|
278
312
|
*/
|
|
279
313
|
export async function fetchSymbolFundingRates(symbol, includeUnverified = false) {
|
|
280
|
-
|
|
314
|
+
// The comparison groups by canonical key: a venue ticker / denomination alias (kPEPE) is
|
|
315
|
+
// looked up under its canonical symbol (1000PEPE), never as-is.
|
|
316
|
+
const snapshot = await fetchAllFundingRates({ symbols: [canonicalQuerySymbol(symbol)], includeUnverified });
|
|
281
317
|
return snapshot.symbols[0] ?? null;
|
|
282
318
|
}
|
|
283
319
|
/**
|
package/dist/liquidity.d.ts
CHANGED
|
@@ -26,7 +26,9 @@ export declare function computeExecutableSize(levels: [string, string][], reques
|
|
|
26
26
|
* Check liquidity on both sides for an arb entry.
|
|
27
27
|
* Returns adjusted size or 0 if not viable.
|
|
28
28
|
*/
|
|
29
|
-
export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void
|
|
29
|
+
export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void,
|
|
30
|
+
/** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
|
|
31
|
+
shortSymbol?: string): Promise<{
|
|
30
32
|
viable: boolean;
|
|
31
33
|
adjustedSizeUsd: number;
|
|
32
34
|
longSlippage: number;
|
package/dist/liquidity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyError } from "./errors.js";
|
|
1
2
|
/**
|
|
2
3
|
* Walk through orderbook levels and compute how much can be filled
|
|
3
4
|
* within the given slippage tolerance.
|
|
@@ -63,11 +64,13 @@ export function computeExecutableSize(levels, requestedSizeUsd, maxSlippagePct =
|
|
|
63
64
|
* Check liquidity on both sides for an arb entry.
|
|
64
65
|
* Returns adjusted size or 0 if not viable.
|
|
65
66
|
*/
|
|
66
|
-
export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeUsd, maxSlippagePct = 0.5, log
|
|
67
|
+
export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeUsd, maxSlippagePct = 0.5, log,
|
|
68
|
+
/** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
|
|
69
|
+
shortSymbol = symbol) {
|
|
67
70
|
try {
|
|
68
71
|
const [longOB, shortOB] = await Promise.all([
|
|
69
72
|
longAdapter.getOrderbook(symbol),
|
|
70
|
-
shortAdapter.getOrderbook(
|
|
73
|
+
shortAdapter.getOrderbook(shortSymbol),
|
|
71
74
|
]);
|
|
72
75
|
// For long entry we consume asks, for short entry we consume bids
|
|
73
76
|
const longCheck = computeExecutableSize(longOB.asks, sizeUsd, maxSlippagePct);
|
|
@@ -145,8 +148,9 @@ export async function checkSpotPerpLiquidity(spotAdapter, perpAdapter, spotSymbo
|
|
|
145
148
|
perpSlippage: perpCheck.slippagePct,
|
|
146
149
|
};
|
|
147
150
|
}
|
|
148
|
-
catch {
|
|
149
|
-
|
|
151
|
+
catch (e) {
|
|
152
|
+
const c = classifyError(e);
|
|
153
|
+
log?.(`[LIQ] spot-perp ${spotSymbol}: orderbook fetch failed (${c.code}: ${c.message})`);
|
|
150
154
|
return { viable: false, adjustedSizeUsd: 0, spotSlippage: 0, perpSlippage: 0 };
|
|
151
155
|
}
|
|
152
156
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { PacificaClient } from "./pacifica/client.js";
|
|
2
|
+
import { PacificaClient, requireSolanaAddress } from "./pacifica/client.js";
|
|
3
3
|
import { fetchPrintGames, fetchPrintHistory, fetchPrintMarket, fetchPrintPositions, findPrintGame, quotePrintOrder } from "./exchanges/pacifica-print.js";
|
|
4
|
-
import { DEPOSIT_WHITELIST_API_STATUS,
|
|
4
|
+
import { DEPOSIT_WHITELIST_API_STATUS, readDepositWhitelist, fetchVaults, findVault, heldShares, withdrawWindowAt } from "./exchanges/pacifica-vaults.js";
|
|
5
5
|
export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
|
|
6
6
|
const client = () => new PacificaClient({ network });
|
|
7
7
|
const run = async (meta, f) => {
|
|
@@ -12,7 +12,13 @@ export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
|
|
|
12
12
|
return { content: [{ type: "text", text: errOf(e, meta) }], isError: true };
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
|
-
const address = z.string().
|
|
15
|
+
const address = z.string().refine((v) => { try {
|
|
16
|
+
requireSolanaAddress(v);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
} }, "base58 Solana address (32-byte public key; not an EVM address)");
|
|
16
22
|
server.tool("pacifica_print_products", "Pacifica Print products (read-only): game id (e.g. BTC_24H, BTC_7D, BTC_1M), cycle length in days, max leverage, deposit caps, allowed target distance from mark. Print = a strike order that earns a premium each cycle; while resting it fills / is liquidated only at a cycle checkpoint, after a fill it is an ordinary perp position (continuous liquidation).", {}, async () => run({ exchange: "pacifica" }, async () => fetchPrintGames(client())));
|
|
17
23
|
server.tool("pacifica_print_quote", "Validate a Pacifica Print order and preview it (read-only, nothing signed): notional, size on fill, docs-formula liquidation price (an outer bound — a liquidation was observed earlier in one live case; venue trigger unpublished), max loss (deposit + premiums), lock period. The premium is not quoted (the venue's preview is a signed request). Placing the order is CLI-only: perp -e pacifica print deposit … (supports --dry-run).", {
|
|
18
24
|
game: z.string().describe("Print product, e.g. BTC_24H"),
|
|
@@ -46,12 +52,12 @@ export function registerPacificaEarnMcpTools(server, network, ok, errOf) {
|
|
|
46
52
|
if (vault === undefined)
|
|
47
53
|
return vaults;
|
|
48
54
|
const v = findVault(vaults, vault);
|
|
49
|
-
const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address),
|
|
55
|
+
const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), readDepositWhitelist(c, v.address)]);
|
|
50
56
|
return {
|
|
51
57
|
...v,
|
|
52
58
|
...(account !== undefined ? { holding: heldShares(v, account) } : {}),
|
|
53
59
|
live: { accountEquity: acct.account_equity ?? null, availableToWithdraw: acct.available_to_withdraw ?? null, positions: positions.map((p) => ({ symbol: p.symbol, side: p.side, amount: p.amount, entryPrice: p.entry_price })) },
|
|
54
|
-
depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist },
|
|
60
|
+
depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist.addresses, error: whitelist.error },
|
|
55
61
|
withdrawWindow: withdrawWindowAt(v.config, Date.now()),
|
|
56
62
|
};
|
|
57
63
|
}));
|
package/dist/mcp-server.js
CHANGED
|
@@ -23,8 +23,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
23
23
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
import { hasCredentials, listConfiguredAccountVenues, resolveCredentials, NO_CLI_CREDENTIAL_OPTS } from "./config.js";
|
|
26
|
-
import { getVenue, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities, roleUnsupported } from "./exchanges/venue.js";
|
|
26
|
+
import { getVenue, listPublicBalanceVenues, listPublicPositionVenues, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities, roleUnsupported } from "./exchanges/venue.js";
|
|
27
27
|
import "./exchanges/venues/index.js";
|
|
28
|
+
import { venueMarketRow } from "./exchanges/venue-types.js";
|
|
29
|
+
import { lighterFundingStatsWarning } from "./exchanges/lighter-public.js";
|
|
28
30
|
import { newVenueClientOrderId, placeAndConfirm, validateLegacyTif, validateOrderRequest } from "./exchanges/trading-v2.js";
|
|
29
31
|
import { closePerpVerified, isOutcomeUnknown } from "./bot/perp-order.js";
|
|
30
32
|
import { quarantineSymbol } from "./bot/quarantine.js";
|
|
@@ -34,7 +36,9 @@ import { PerpError, classifyError, extractErrorMessage, isDefiniteOrderFailure,
|
|
|
34
36
|
import { invalidateCache } from "./cache.js";
|
|
35
37
|
import { probeVenueHealth } from "./venue-health.js";
|
|
36
38
|
import { getOptionsVenue, listOptionsVenueIds } from "./exchanges/options-venues.js";
|
|
37
|
-
import { fetchAllFundingRates } from "./funding-rates.js";
|
|
39
|
+
import { fetchAllFundingRates, isRankedComparison } from "./funding-rates.js";
|
|
40
|
+
import { readPositionsByAddress } from "./positions-by-address.js";
|
|
41
|
+
import { readBalanceByAddress, readBalanceByAddressAll } from "./balance-by-address.js";
|
|
38
42
|
import { loadArbState } from "./arb/state.js";
|
|
39
43
|
import { validateTrade } from "./trade-validator.js";
|
|
40
44
|
import { orderResultJson, symbolMatch } from "./utils.js";
|
|
@@ -130,15 +134,38 @@ async function tryGetAdapter(exchange) {
|
|
|
130
134
|
async function fetchMarketsPublic(exchange) {
|
|
131
135
|
return labelMarketRows(exchange, await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK));
|
|
132
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* meta.warnings when a venue's rows carry fundingRate null for a known reason (Lighter: the
|
|
139
|
+
* settled-rate snapshot could not be read) — the reason reaches the agent, not only stderr.
|
|
140
|
+
*/
|
|
141
|
+
function fundingWarningMeta(exchange, rows) {
|
|
142
|
+
const id = getVenue(exchange).id;
|
|
143
|
+
if (id !== "lighter" && id !== "lighter-rh")
|
|
144
|
+
return {};
|
|
145
|
+
// Tied to the rows served: only when they actually carry a null rate (cached rows that have
|
|
146
|
+
// rates never get a stale warning; null rows always say why when the reason is known).
|
|
147
|
+
if (!rows.some(r => r.fundingRate === null))
|
|
148
|
+
return {};
|
|
149
|
+
const w = lighterFundingStatsWarning(id, MCP_NETWORK);
|
|
150
|
+
return w === null ? {} : { warnings: [w] };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Keyless get_markets rows: the public directory's VenueMarket rows in the CLI market-row shape
|
|
154
|
+
* (venueMarketRow — the mapper the VenueMarket CLI adapters use), the VenueMarket fields kept
|
|
155
|
+
* (venueSymbol, canonical, assetClass, fundingRateBasis, tickSize, …) and the MCP labels added.
|
|
156
|
+
*/
|
|
157
|
+
async function fetchMarketRowsPublic(exchange) {
|
|
158
|
+
const markets = await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK);
|
|
159
|
+
return labelMarketRows(exchange, markets.map(m => ({ ...m, ...venueMarketRow(m) })));
|
|
160
|
+
}
|
|
133
161
|
/**
|
|
134
162
|
* The one row label for market rows of any source (public data path or a credentialed adapter):
|
|
135
|
-
*
|
|
163
|
+
* every row carries fundingVerified (true or false — as `perp market list`), unofficial-API rows venueApi.
|
|
136
164
|
*/
|
|
137
165
|
function labelMarketRows(exchange, rows) {
|
|
138
166
|
const def = getVenue(exchange);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return def.fundingVerified ? rows : rows.map(r => ({ ...r, fundingVerified: false }));
|
|
167
|
+
const labelled = rows.map(r => ({ ...r, fundingVerified: def.fundingVerified }));
|
|
168
|
+
return def.apiStatus.kind === "unofficial" ? labelled.map(r => ({ ...r, venueApi: "unofficial" })) : labelled;
|
|
142
169
|
}
|
|
143
170
|
/**
|
|
144
171
|
* meta fields every market-data tool adds for its venue: apiStatus, the note for an unofficial API,
|
|
@@ -261,10 +288,13 @@ server.tool("get_markets", `Get all available perpetual futures markets on an ex
|
|
|
261
288
|
const adapter = await tryGetAdapter(exchange);
|
|
262
289
|
if (adapter) {
|
|
263
290
|
const markets = labelMarketRows(exchange, await adapter.getMarkets());
|
|
264
|
-
return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, ...apiMeta(exchange) }) }] };
|
|
291
|
+
return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
|
|
265
292
|
}
|
|
266
|
-
|
|
267
|
-
|
|
293
|
+
// No credentials: the venue's public directory — price-only on GRVT (no 184-request
|
|
294
|
+
// margin_rules fan-out per call) — in the CLI market-row shape (venueMarketRow), with the
|
|
295
|
+
// same market set as `perp market list` (the venue's shared eligibility / listing rule).
|
|
296
|
+
const markets = await fetchMarketRowsPublic(exchange);
|
|
297
|
+
return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public", ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
|
|
268
298
|
}
|
|
269
299
|
catch (e) {
|
|
270
300
|
return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
|
|
@@ -329,6 +359,10 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
|
|
|
329
359
|
return;
|
|
330
360
|
}
|
|
331
361
|
for (const m of r.value) {
|
|
362
|
+
// An ambiguous ticker (SPX6900 vs the S&P 500) names different instruments per venue:
|
|
363
|
+
// never grouped by symbol across venues.
|
|
364
|
+
if (m.assetClass === "unclassified")
|
|
365
|
+
continue;
|
|
332
366
|
const sym = m.symbol.toUpperCase();
|
|
333
367
|
if (filter && !filter.has(sym))
|
|
334
368
|
continue;
|
|
@@ -477,6 +511,49 @@ server.tool("get_positions", `Get all open positions on an exchange, including s
|
|
|
477
511
|
return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
|
|
478
512
|
}
|
|
479
513
|
});
|
|
514
|
+
server.tool("get_positions_by_address", `Open positions of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicPositionVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}. Hyperliquid reads the native dex and every HIP-3 dex (rows carry dex: null | "<dex>"; a dex that fails is listed in meta.errors / meta.warnings — its rows are missing, not absent); Lighter reads every account of the address (rows carry accountIndex; accountIndex narrows). Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same rows as \`perp -e <venue> account positions --address <addr>\`.`, {
|
|
515
|
+
exchange: z.string().describe(`Exchange: ${listPublicPositionVenues().map(v => v.id).join(", ")} (aliases accepted)`),
|
|
516
|
+
address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh; base58 Solana for pacifica)"),
|
|
517
|
+
symbol: z.string().optional().describe("Only this market (plain asset, e.g. BTC)"),
|
|
518
|
+
accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
|
|
519
|
+
}, async ({ exchange, address, symbol, accountIndex }) => {
|
|
520
|
+
try {
|
|
521
|
+
const r = await readPositionsByAddress(exchange, MCP_NETWORK, address, { symbol, accountIndex });
|
|
522
|
+
return { content: [{ type: "text", text: ok(r.positions, {
|
|
523
|
+
exchange: r.venue, address, source: "public", count: r.positions.length,
|
|
524
|
+
...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
|
|
525
|
+
...(r.notes.length > 0 ? { notes: r.notes } : {}),
|
|
526
|
+
}) }] };
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
server.tool("get_balance_by_address", `Balances of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicBalanceVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}; omit exchange to read every one of them (a venue that fails, address format included, is listed in data.errors). data: { balance (equity / available / marginUsed / unrealizedPnl + venue extras; null on Lighter), spot (token balances; null where the venue does not publish them), accounts (Lighter: one row per account of the address; accountIndex narrows), dexes + accountMode (Hyperliquid: HIP-3 dexes with a non-zero account value; a dex that fails is in meta.errors / meta.warnings — unknown, not zero) }. Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same data as \`perp [-e <venue>] account balance --address <addr>\`.`, {
|
|
533
|
+
exchange: z.string().optional().describe(`Exchange: ${listPublicBalanceVenues().map(v => v.id).join(", ")} (aliases accepted); omit for every one`),
|
|
534
|
+
address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh / risex; base58 Solana for pacifica)"),
|
|
535
|
+
accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
|
|
536
|
+
}, async ({ exchange, address, accountIndex }) => {
|
|
537
|
+
try {
|
|
538
|
+
if (exchange === undefined) {
|
|
539
|
+
const all = await readBalanceByAddressAll(MCP_NETWORK, address, { accountIndex });
|
|
540
|
+
return { content: [{ type: "text", text: ok({
|
|
541
|
+
venues: all.venues,
|
|
542
|
+
...(Object.keys(all.errors).length > 0 ? { errors: all.errors } : {}),
|
|
543
|
+
...(Object.keys(all.notes).length > 0 ? { notes: all.notes } : {}),
|
|
544
|
+
}, { address, source: "public", ...(all.warnings.length > 0 ? { warnings: all.warnings } : {}) }) }] };
|
|
545
|
+
}
|
|
546
|
+
const r = await readBalanceByAddress(exchange, MCP_NETWORK, address, { accountIndex });
|
|
547
|
+
return { content: [{ type: "text", text: ok({ exchange: r.venue, address, ...r.data }, {
|
|
548
|
+
exchange: r.venue, address, source: "public",
|
|
549
|
+
...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
|
|
550
|
+
...(r.notes.length > 0 ? { notes: r.notes } : {}),
|
|
551
|
+
}) }] };
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
|
|
555
|
+
}
|
|
556
|
+
});
|
|
480
557
|
server.tool("get_open_orders", `Get all open/pending orders on an exchange. ${HL_SCOPE_NOTE}`, { exchange: z.string().describe(`Exchange name: ${listVenues().map(v => v.id).join(", ")} (aliases accepted)`) }, async ({ exchange }) => {
|
|
481
558
|
try {
|
|
482
559
|
const adapter = await getOrCreateAdapter(exchange);
|
|
@@ -510,6 +587,9 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
|
|
|
510
587
|
};
|
|
511
588
|
}
|
|
512
589
|
}));
|
|
590
|
+
// Totals are null (unknown) when no account balance was read — no configured credentials, or
|
|
591
|
+
// every venue failed — never 0 (QA 0.15.0: totalEquity 0 without credentials).
|
|
592
|
+
const read = snapshots.filter(s => s.balance !== null);
|
|
513
593
|
let totalEquity = 0;
|
|
514
594
|
let totalAvailable = 0;
|
|
515
595
|
let totalMarginUsed = 0;
|
|
@@ -545,16 +625,21 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
|
|
|
545
625
|
pct: totalEquity > 0 ? (Number(s.balance.equity) / totalEquity) * 100 : 0,
|
|
546
626
|
}))
|
|
547
627
|
.sort((a, b) => b.pct - a.pct);
|
|
628
|
+
const known = read.length > 0;
|
|
548
629
|
const summary = {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
630
|
+
/** "noAccount" = no venue has credentials; "unread" = credentials exist but no balance was read. */
|
|
631
|
+
accountState: EXCHANGES.length === 0 ? "noAccount" : known ? "ok" : "unread",
|
|
632
|
+
/** Venues whose balances the totals cover. */
|
|
633
|
+
totalsCover: read.map(s => s.exchange),
|
|
634
|
+
totalEquity: known ? totalEquity : null,
|
|
635
|
+
totalAvailable: known ? totalAvailable : null,
|
|
636
|
+
totalMarginUsed: known ? totalMarginUsed : null,
|
|
637
|
+
totalUnrealizedPnl: known ? totalUnrealizedPnl : null,
|
|
553
638
|
totalPositions,
|
|
554
639
|
totalOpenOrders,
|
|
555
640
|
exchanges: snapshots,
|
|
556
641
|
positions: allPositions,
|
|
557
|
-
riskMetrics: { marginUtilization, largestPosition, exchangeConcentration },
|
|
642
|
+
riskMetrics: { marginUtilization: known ? marginUtilization : null, largestPosition, exchangeConcentration },
|
|
558
643
|
};
|
|
559
644
|
return { content: [{ type: "text", text: ok(summary) }] };
|
|
560
645
|
});
|
|
@@ -1198,13 +1283,14 @@ server.tool("arb_scan", "Scan for funding rate arbitrage opportunities across ex
|
|
|
1198
1283
|
symbols,
|
|
1199
1284
|
minSpread: minSpread ?? 5,
|
|
1200
1285
|
});
|
|
1201
|
-
const opportunities = snapshot.symbols.map(s => ({
|
|
1286
|
+
const opportunities = snapshot.symbols.filter(isRankedComparison).map(s => ({
|
|
1202
1287
|
symbol: s.symbol,
|
|
1203
1288
|
maxSpreadAnnual: `${s.maxSpreadAnnual.toFixed(2)}%`,
|
|
1204
1289
|
strategy: `Long ${s.longExchange} / Short ${s.shortExchange}`,
|
|
1205
1290
|
// false = a leg is on a read-only venue (RISEx in this release): data only, not tradable here.
|
|
1206
1291
|
executable: s.executable,
|
|
1207
1292
|
readOnlyLegs: s.readOnlyLegs,
|
|
1293
|
+
unverifiedLegs: s.unverifiedLegs,
|
|
1208
1294
|
estHourlyIncomePerK: `$${s.estHourlyIncomeUsd.toFixed(4)}`,
|
|
1209
1295
|
rates: s.rates.map(r => ({
|
|
1210
1296
|
exchange: r.exchange,
|
|
@@ -1236,6 +1322,9 @@ server.tool("health_check", "Ping every registered venue on the server's network
|
|
|
1236
1322
|
result.covered = health.probes.map(p => p.venue);
|
|
1237
1323
|
// A venue without an environment on the server's network (TxFlow has no testnet) is not probed.
|
|
1238
1324
|
result.skipped = health.skipped;
|
|
1325
|
+
// Same two verdicts as `perp health --json`: healthy = every OFFICIAL-API venue is up (an
|
|
1326
|
+
// unofficial API being down does not make perp-cli unhealthy); allHealthy includes them.
|
|
1327
|
+
result.healthy = health.probes.filter(p => p.apiStatus === "official").every(p => p.ok);
|
|
1239
1328
|
result.allHealthy = health.allHealthy;
|
|
1240
1329
|
return { content: [{ type: "text", text: ok(result) }] };
|
|
1241
1330
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Network } from "./constants.js";
|
|
2
|
+
import { requireSolanaAddress } from "../exchanges/address.js";
|
|
2
3
|
import type { MarketInfo, PriceInfo, Orderbook, Trade, Kline, KlineInterval, AggLevel, AccountInfo, AccountSettings, Position, OrderInfo, MarketOrderParams, LimitOrderParams, StopOrderParams, EditOrderParams, CancelOrderParams, CancelAllOrdersParams, TWAPParams, CancelTWAPParams, TPSLParams, UpdateLeverageParams, WithdrawParams, TransferFundsParams, BatchAction, BatchActionType, CreateLakeParams, LakeDepositParams, LakeWithdrawParams } from "./types/index.js";
|
|
3
4
|
type SignMessageFn = (message: Uint8Array) => Promise<Uint8Array>;
|
|
4
5
|
/** Per-request timeout of a POST (fetch + body read). */
|
|
@@ -16,6 +17,7 @@ export interface PacificaClientConfig {
|
|
|
16
17
|
baseUrl?: string;
|
|
17
18
|
apiKey?: string;
|
|
18
19
|
}
|
|
20
|
+
export { requireSolanaAddress };
|
|
19
21
|
export declare class PacificaClient {
|
|
20
22
|
private baseUrl;
|
|
21
23
|
private apiKey?;
|
|
@@ -202,4 +204,3 @@ export declare class PacificaClient {
|
|
|
202
204
|
expiry_window: number;
|
|
203
205
|
}): Promise<Record<string, unknown>>;
|
|
204
206
|
}
|
|
205
|
-
export {};
|
package/dist/pacifica/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getNetworkConfig } from "./constants.js";
|
|
|
2
2
|
import { buildSignedRequest, buildAgentSignedRequest, signWithWallet } from "./signing.js";
|
|
3
3
|
import { PerpError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
|
|
4
4
|
import { invalidateCache } from "../cache.js";
|
|
5
|
+
import { requireSolanaAddress } from "../exchanges/address.js";
|
|
5
6
|
import { PACIFICA_COST_CANCEL, PACIFICA_COST_STANDARD, PACIFICA_MAX_THROTTLE_WAIT_MS, PACIFICA_READ_NEED, PACIFICA_SIGNED_MAX_WAIT_MS, pacificaRateLimiter, } from "./rate-limit.js";
|
|
6
7
|
/** Per-request timeout of a POST (fetch + body read). */
|
|
7
8
|
export const PACIFICA_POST_TIMEOUT_MS = 15_000;
|
|
@@ -38,6 +39,14 @@ function writeNeed(cancel, maxWaitMs) {
|
|
|
38
39
|
export function pacificaBatchCost(types) {
|
|
39
40
|
return types.reduce((a, t) => a + (t === "Cancel" || t === "CancelStopOrder" ? PACIFICA_COST_CANCEL : PACIFICA_COST_STANDARD), 0);
|
|
40
41
|
}
|
|
42
|
+
/** Query parameters that carry a Solana address (account reads, Print orders, vaults). */
|
|
43
|
+
const ADDRESS_PARAMS = new Set(["account", "game_account", "lake"]);
|
|
44
|
+
export { requireSolanaAddress };
|
|
45
|
+
function checkAddressParams(params) {
|
|
46
|
+
for (const [k, v] of Object.entries(params ?? {}))
|
|
47
|
+
if (ADDRESS_PARAMS.has(k) && v !== undefined && v !== null)
|
|
48
|
+
requireSolanaAddress(v, k);
|
|
49
|
+
}
|
|
41
50
|
export class PacificaClient {
|
|
42
51
|
baseUrl;
|
|
43
52
|
apiKey;
|
|
@@ -87,6 +96,7 @@ export class PacificaClient {
|
|
|
87
96
|
return h;
|
|
88
97
|
}
|
|
89
98
|
async get(path, params) {
|
|
99
|
+
checkAddressParams(params);
|
|
90
100
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
91
101
|
if (params) {
|
|
92
102
|
Object.entries(params).forEach(([k, v]) => {
|
|
@@ -445,6 +455,7 @@ export class PacificaClient {
|
|
|
445
455
|
* non-JSON / non-object body → EXCHANGE_ERROR (a read: nothing can have changed).
|
|
446
456
|
*/
|
|
447
457
|
async getEnvelope(path, params = {}) {
|
|
458
|
+
checkAddressParams(params);
|
|
448
459
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
449
460
|
for (const [k, v] of Object.entries(params))
|
|
450
461
|
url.searchParams.set(k, v);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Network, type PublicPosition } from "./exchanges/venue.js";
|
|
2
|
+
export interface AddressPositions {
|
|
3
|
+
venue: string;
|
|
4
|
+
positions: PublicPosition[];
|
|
5
|
+
/** Sub-reads that failed (Hyperliquid HIP-3 dex), keyed "hip3:<dex>". */
|
|
6
|
+
errors: Record<string, {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}>;
|
|
10
|
+
notes: string[];
|
|
11
|
+
/** One human line per failed sub-read: its rows are unknown, not absent. */
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface AddressPositionsOptions {
|
|
15
|
+
accountIndex?: number;
|
|
16
|
+
symbol?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Rows of `symbol` (plain asset; HIP-3 "xyz:GOLD" also matches "GOLD"; "BTC-PERP" matches "BTC"). */
|
|
19
|
+
export declare function filterPositionsBySymbol<T extends {
|
|
20
|
+
symbol: string;
|
|
21
|
+
}>(rows: T[], symbol: string | undefined): T[];
|
|
22
|
+
export declare function readPositionsByAddress(venue: string, net: Network, address: string, opts?: AddressPositionsOptions): Promise<AddressPositions>;
|
|
23
|
+
export interface AddressPositionsFanOut {
|
|
24
|
+
venues: Record<string, PublicPosition[]>;
|
|
25
|
+
/** A venue that failed (`<venue>`) or a failed sub-read of a venue (`<venue>:hip3:<dex>`). */
|
|
26
|
+
errors: Record<string, {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
30
|
+
notes: Record<string, string[]>;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Every venue that reads positions by address; a venue that fails (address format included) is an `errors` entry. */
|
|
34
|
+
export declare function readPositionsByAddressAll(net: Network, address: string, opts?: AddressPositionsOptions): Promise<AddressPositionsFanOut>;
|