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
|
@@ -19,7 +19,7 @@ import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
|
|
|
19
19
|
import { PerpError } from "../errors.js";
|
|
20
20
|
import { PacificaAdapter } from "../exchanges/pacifica.js";
|
|
21
21
|
import { resolveVenueId } from "../exchanges/venue.js";
|
|
22
|
-
import { PacificaClient } from "../pacifica/client.js";
|
|
22
|
+
import { PacificaClient, requireSolanaAddress } from "../pacifica/client.js";
|
|
23
23
|
import { logExecution } from "../execution-log.js";
|
|
24
24
|
import { jsonOk, makeTable, printJson } from "../utils.js";
|
|
25
25
|
import { mulDecimal } from "../exchanges/precision.js";
|
|
@@ -27,7 +27,7 @@ import { cmpDecimal, isPlainDecimal, trimDecimal } from "../exchanges/pacifica-d
|
|
|
27
27
|
import { fundsFailStatus } from "./funds.js";
|
|
28
28
|
import { requireIdempotencyKey, restampPresigned, sendPresigned, signPresigned } from "../exchanges/pacifica-presigned.js";
|
|
29
29
|
import { buildPrintDepositRequest, buildPrintEndRequest, daysUntil, fetchPrintEvents, fetchPrintGames, fetchPrintHistory, fetchPrintMarket, fetchPrintPositions, findPrintGame, quotePrintOrder, requireEndable, } from "../exchanges/pacifica-print.js";
|
|
30
|
-
import { buildVaultDepositRequest, buildVaultWithdrawRequest, DEPOSIT_WHITELIST_API_STATUS,
|
|
30
|
+
import { buildVaultDepositRequest, buildVaultWithdrawRequest, DEPOSIT_WHITELIST_API_STATUS, readDepositWhitelist, fetchVaults, findVault, heldShares, planVaultDeposit, planVaultWithdraw, withdrawWindowAt, } from "../exchanges/pacifica-vaults.js";
|
|
31
31
|
const ts = (ms) => (ms === null ? "—" : new Date(ms).toISOString().replace("T", " ").slice(0, 16) + " UTC");
|
|
32
32
|
export function registerPacificaEarnCommands(program, getAdapterForExchange, isJson) {
|
|
33
33
|
/** Pacifica-only: an explicit -e naming another venue is refused (never silently ignored). */
|
|
@@ -288,11 +288,11 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
|
|
|
288
288
|
assertPacifica();
|
|
289
289
|
const c = publicClient();
|
|
290
290
|
const v = findVault(await fetchVaults(c, null), ref);
|
|
291
|
-
const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address),
|
|
291
|
+
const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), readDepositWhitelist(c, v.address)]);
|
|
292
292
|
const out = {
|
|
293
293
|
...v,
|
|
294
294
|
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 })) },
|
|
295
|
-
depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist },
|
|
295
|
+
depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist.addresses, error: whitelist.error },
|
|
296
296
|
withdrawWindow: withdrawWindowAt(v.config, Date.now()),
|
|
297
297
|
};
|
|
298
298
|
if (isJson())
|
|
@@ -304,7 +304,7 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
|
|
|
304
304
|
"vault free balance": out.live.availableToWithdraw ?? "—", "performance fee": v.config.managerProfitShare ?? "none", "deposit cap": v.config.depositCap ?? "none",
|
|
305
305
|
"lock-up after deposit": v.config.depositMinDurationMs === null ? "none" : `${(v.config.depositMinDurationMs / 3_600_000).toFixed(2)} h (reset by every deposit)`,
|
|
306
306
|
"withdrawal window": `${windowLabel(v)}${out.withdrawWindow.restricted ? ` — now ${out.withdrawWindow.open ? "OPEN until" : "closed, opens"} ${ts(out.withdrawWindow.nextChangeAt)}` : ""}`,
|
|
307
|
-
"deposit whitelist": `${whitelist === null ?
|
|
307
|
+
"deposit whitelist": `${whitelist.addresses === null ? `unknown (${whitelist.error})` : whitelist.addresses.length === 0 ? "none (anyone)" : `${whitelist.addresses.length} address(es)`} (unofficial endpoint; the venue decides)`, APY: "not published",
|
|
308
308
|
}))
|
|
309
309
|
console.log(` ${k.padEnd(22)} ${val}`);
|
|
310
310
|
if (out.live.positions.length > 0)
|
|
@@ -339,8 +339,8 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
|
|
|
339
339
|
const c = publicClient();
|
|
340
340
|
const s = await masterSigning(o.account, dryRun);
|
|
341
341
|
const v = findVault(await fetchVaults(c, s.identity.account), ref);
|
|
342
|
-
const [info, whitelist] = await Promise.all([c.getAccount(s.identity.account),
|
|
343
|
-
const plan = planVaultDeposit(v, s.identity.account, amount, { availableToWithdraw: availableOf(info, s.identity.account), whitelist });
|
|
342
|
+
const [info, whitelist] = await Promise.all([c.getAccount(s.identity.account), readDepositWhitelist(c, v.address)]);
|
|
343
|
+
const plan = planVaultDeposit(v, s.identity.account, amount, { availableToWithdraw: availableOf(info, s.identity.account), whitelist: whitelist.addresses });
|
|
344
344
|
const sharesBefore = heldShares(v, s.identity.account).shares ?? "0";
|
|
345
345
|
const req = buildVaultDepositRequest(v.address, plan.amount, s.identity, { timestamp: Date.now(), idempotencyKey });
|
|
346
346
|
if (!isJson())
|
|
@@ -393,21 +393,8 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
|
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
395
|
// ── helpers ──────────────────────────────────────────────────────────────
|
|
396
|
-
/** Unofficial whitelist read: null when unreadable (advisory only — the venue's refusal is final). */
|
|
397
|
-
async function whitelistOrNull(c, vault) {
|
|
398
|
-
try {
|
|
399
|
-
return await fetchDepositWhitelist(c, vault);
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
396
|
function requireAddress(s) {
|
|
406
|
-
|
|
407
|
-
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a)) {
|
|
408
|
-
throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) address: "${s}"`, { remediation: "Pass a base58 Solana address" });
|
|
409
|
-
}
|
|
410
|
-
return a;
|
|
397
|
+
return requireSolanaAddress(s);
|
|
411
398
|
}
|
|
412
399
|
function parseDirection(s) {
|
|
413
400
|
const d = s.trim().toLowerCase();
|
|
@@ -16,10 +16,26 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { Command } from "commander";
|
|
18
18
|
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
19
|
+
import type { DashboardExchange } from "../dashboard/index.js";
|
|
19
20
|
import "../exchanges/venues/index.js";
|
|
21
|
+
/**
|
|
22
|
+
* HIP-3 dex names, read live from info `perpDexs` on the selected network (index 0 = the native
|
|
23
|
+
* dex, null). Throws on a failed or malformed read — never substitutes a fixed list (Rule #2):
|
|
24
|
+
* a stale list hides positions on any dex Hyperliquid adds later.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fetchHip3DexNames(): Promise<string[]>;
|
|
20
27
|
/**
|
|
21
28
|
* Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
|
|
22
29
|
* The failure is warned on stderr; callers must treat null as "may hold positions".
|
|
23
30
|
*/
|
|
24
31
|
export declare function checkDexPositions(address: string, dex: string): Promise<number | null>;
|
|
32
|
+
/**
|
|
33
|
+
* HIP-3 dexes holding positions (or whose read failed) for `address`, scanning every dex of the
|
|
34
|
+
* live `perpDexs` list. `error` is set when the dex list itself could not be read: no dex is
|
|
35
|
+
* scanned and the caller must surface it (positions on HIP-3 dexes are unknown, not absent).
|
|
36
|
+
*/
|
|
37
|
+
export declare function autoDetectHip3Dexes(address: string, getHLAdapterForDex: (dex: string) => Promise<ExchangeAdapter>, verbose: boolean): Promise<{
|
|
38
|
+
exchanges: DashboardExchange[];
|
|
39
|
+
error: string | null;
|
|
40
|
+
}>;
|
|
25
41
|
export declare function registerPortfolioCommand(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, version: string, getHLAdapterForDex?: (dex: string) => Promise<ExchangeAdapter>): void;
|
|
@@ -304,7 +304,7 @@ async function runHealth(exchangeList, network) {
|
|
|
304
304
|
}
|
|
305
305
|
// ── Arb top-5 ──────────────────────────────────────────────────────────────
|
|
306
306
|
async function fetchArbTop() {
|
|
307
|
-
const { fetchAllFundingRates, TOP_SYMBOLS } = await import("../funding-rates.js");
|
|
307
|
+
const { fetchAllFundingRates, isRankedComparison, TOP_SYMBOLS } = await import("../funding-rates.js");
|
|
308
308
|
const { saveFundingSnapshot, getHistoricalAverages } = await import("../funding-history.js");
|
|
309
309
|
let snapshot;
|
|
310
310
|
try {
|
|
@@ -323,6 +323,7 @@ async function fetchArbTop() {
|
|
|
323
323
|
}
|
|
324
324
|
catch { /* ignore */ }
|
|
325
325
|
const top = snapshot.symbols
|
|
326
|
+
.filter(isRankedComparison)
|
|
326
327
|
.filter(s => s.maxSpreadAnnual >= 5)
|
|
327
328
|
.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual)
|
|
328
329
|
.slice(0, 5);
|
|
@@ -527,7 +528,31 @@ async function runWatch(resolved, version, getAdapterForExchange, isJsonMode) {
|
|
|
527
528
|
}
|
|
528
529
|
}
|
|
529
530
|
// ── Serve mode (delegates to dashboard infra) ──────────────────────────────
|
|
530
|
-
|
|
531
|
+
/**
|
|
532
|
+
* HIP-3 dex names, read live from info `perpDexs` on the selected network (index 0 = the native
|
|
533
|
+
* dex, null). Throws on a failed or malformed read — never substitutes a fixed list (Rule #2):
|
|
534
|
+
* a stale list hides positions on any dex Hyperliquid adds later.
|
|
535
|
+
*/
|
|
536
|
+
export async function fetchHip3DexNames() {
|
|
537
|
+
const res = await fetch(HYPERLIQUID_API_URL, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "Content-Type": "application/json" },
|
|
540
|
+
body: JSON.stringify({ type: "perpDexs" }),
|
|
541
|
+
});
|
|
542
|
+
if (!res.ok)
|
|
543
|
+
throw new Error(`info perpDexs HTTP ${res.status}`);
|
|
544
|
+
const list = await res.json();
|
|
545
|
+
if (!Array.isArray(list))
|
|
546
|
+
throw new Error("info perpDexs: expected an array");
|
|
547
|
+
return list.flatMap((d, i) => {
|
|
548
|
+
if (d === null)
|
|
549
|
+
return [];
|
|
550
|
+
const name = d?.name;
|
|
551
|
+
if (typeof name !== "string" || name === "")
|
|
552
|
+
throw new Error(`info perpDexs: entry ${i} has no dex name`);
|
|
553
|
+
return [name];
|
|
554
|
+
});
|
|
555
|
+
}
|
|
531
556
|
/**
|
|
532
557
|
* Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
|
|
533
558
|
* The failure is warned on stderr; callers must treat null as "may hold positions".
|
|
@@ -558,8 +583,22 @@ export async function checkDexPositions(address, dex) {
|
|
|
558
583
|
return null;
|
|
559
584
|
}
|
|
560
585
|
}
|
|
561
|
-
|
|
562
|
-
|
|
586
|
+
/**
|
|
587
|
+
* HIP-3 dexes holding positions (or whose read failed) for `address`, scanning every dex of the
|
|
588
|
+
* live `perpDexs` list. `error` is set when the dex list itself could not be read: no dex is
|
|
589
|
+
* scanned and the caller must surface it (positions on HIP-3 dexes are unknown, not absent).
|
|
590
|
+
*/
|
|
591
|
+
export async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
592
|
+
let dexes;
|
|
593
|
+
try {
|
|
594
|
+
dexes = await fetchHip3DexNames();
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
const error = `HIP-3 dex list unavailable — HIP-3 positions unknown, pass --dexes to monitor specific dexes: ${e instanceof Error ? e.message : String(e)}`;
|
|
598
|
+
process.stderr.write(`[warn] hl:hip3 ${error}\n`);
|
|
599
|
+
return { exchanges: [], error };
|
|
600
|
+
}
|
|
601
|
+
const checks = await Promise.all(dexes.map(async (dex) => ({
|
|
563
602
|
dex,
|
|
564
603
|
count: await checkDexPositions(address, dex),
|
|
565
604
|
})));
|
|
@@ -580,7 +619,7 @@ async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
|
|
|
580
619
|
process.stderr.write(`[warn] hl:${dex} left out of the dashboard (adapter unavailable${count === null ? ", positions unknown" : `, ${count} position(s)`}): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
581
620
|
}
|
|
582
621
|
}
|
|
583
|
-
return results;
|
|
622
|
+
return { exchanges: results, error: null };
|
|
584
623
|
}
|
|
585
624
|
async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapterForDex) {
|
|
586
625
|
const { startDashboard } = await import("../dashboard/index.js");
|
|
@@ -616,7 +655,9 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
616
655
|
const hlAddr = hlEx ? hlEx.adapter.address ?? "" : "";
|
|
617
656
|
if (hlAddr) {
|
|
618
657
|
const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, false);
|
|
619
|
-
exchanges.push(...detected);
|
|
658
|
+
exchanges.push(...detected.exchanges);
|
|
659
|
+
if (detected.error !== null)
|
|
660
|
+
errors["hl:hip3"] = detected.error;
|
|
620
661
|
}
|
|
621
662
|
}
|
|
622
663
|
}
|
|
@@ -675,8 +716,10 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
|
|
|
675
716
|
if (hlAddr) {
|
|
676
717
|
console.log(chalk.gray(" Scanning HIP-3 dexes for active positions..."));
|
|
677
718
|
const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, true);
|
|
678
|
-
exchanges.push(...detected);
|
|
679
|
-
if (
|
|
719
|
+
exchanges.push(...detected.exchanges);
|
|
720
|
+
if (detected.error !== null)
|
|
721
|
+
console.log(chalk.yellow(` ✗ hl:hip3: ${detected.error.slice(0, 120)}`));
|
|
722
|
+
else if (!detected.exchanges.length)
|
|
680
723
|
console.log(chalk.gray(" No active HIP-3 dex positions found"));
|
|
681
724
|
}
|
|
682
725
|
}
|
package/dist/commands/trade.js
CHANGED
|
@@ -303,7 +303,7 @@ function checkTpSlDirection(positionSide, ref, refLabel, p) {
|
|
|
303
303
|
}
|
|
304
304
|
/** The venue's current mark price for `symbol` (PRICE_STALE when it is not published / not positive). */
|
|
305
305
|
async function markPriceOf(adapter, symbol) {
|
|
306
|
-
const m = (await adapter.getMarkets()).find(x => symbolMatch(x.symbol, symbol));
|
|
306
|
+
const m = (await adapter.getMarkets({ funding: false })).find(x => symbolMatch(x.symbol, symbol));
|
|
307
307
|
const px = m === undefined ? NaN : Number(m.markPrice);
|
|
308
308
|
if (!(px > 0) || !Number.isFinite(px)) {
|
|
309
309
|
throw new PerpError("PRICE_STALE", `${adapter.name} ${symbol}: no mark price to check the TP/SL direction against`, { exchange: adapter.name });
|
|
@@ -489,7 +489,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
489
489
|
}
|
|
490
490
|
/** Mark price of `sym` (split sizing / risk); no usable mark → PRICE_STALE, nothing sent. */
|
|
491
491
|
async function splitMarkPrice(adapter, sym) {
|
|
492
|
-
const market = (await adapter.getMarkets()).find(m => symbolMatch(m.symbol, sym));
|
|
492
|
+
const market = (await adapter.getMarkets({ funding: false })).find(m => symbolMatch(m.symbol, sym));
|
|
493
493
|
const markPrice = market !== undefined ? Number(market.markPrice) : NaN;
|
|
494
494
|
if (!(markPrice > 0)) {
|
|
495
495
|
throw new PerpError("PRICE_STALE", `${adapter.name}: no usable mark price for ${sym} — cannot size the split; nothing was sent`, { exchange: adapter.name });
|
|
@@ -1489,8 +1489,8 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
|
|
|
1489
1489
|
});
|
|
1490
1490
|
// === Pacifica-only commands ===
|
|
1491
1491
|
supportsDryRun(trade.command("edit <symbol> <orderId> <price> <size>"))
|
|
1492
|
-
.description("Edit an existing order: new price and new open (unfilled) size; the order keeps its TIF / post-only (pacifica:
|
|
1493
|
-
.option("--tif <tif>", "Time-in-force of the replacement (
|
|
1492
|
+
.description("Edit an existing order: new price and new open (unfilled) size; the order keeps its TIF / post-only (pacifica: TIF read from the order's client id — ALO via native edit, GTC / TOB via cancel+replace; a new order id either way) — use the orderId the edit returns; orderId, or c:<clientOrderId> on ondo/arcus/qfex (qfex: plain LIMIT / ALO only; the venue assigns a new order id, the client id is kept)")
|
|
1493
|
+
.option("--tif <tif>", "Time-in-force of the replacement (ondo / pacifica only; ALO keeps post-only)")
|
|
1494
1494
|
.option("--force-tif", "Allow --tif to flip the order's post-only flag (ondo / pacifica); otherwise such a conflict is refused")
|
|
1495
1495
|
.action(async (symbol, orderId, price, size, opts) => {
|
|
1496
1496
|
const adapter = await getAdapter();
|
|
@@ -70,4 +70,6 @@ export declare function marginEntryBlocks(check: ChainMarginCheck, minMarginPct?
|
|
|
70
70
|
* @param maxSlippagePct - Max acceptable slippage (default: 0.3%)
|
|
71
71
|
* @returns USD size per leg, or 0 if not viable
|
|
72
72
|
*/
|
|
73
|
-
export declare function computeAutoSize(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, maxSlippagePct?: number
|
|
73
|
+
export declare function computeAutoSize(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, maxSlippagePct?: number,
|
|
74
|
+
/** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
|
|
75
|
+
shortSymbol?: string): Promise<number>;
|
|
@@ -94,11 +94,13 @@ export function marginEntryBlocks(check, minMarginPct = 30) {
|
|
|
94
94
|
* @param maxSlippagePct - Max acceptable slippage (default: 0.3%)
|
|
95
95
|
* @returns USD size per leg, or 0 if not viable
|
|
96
96
|
*/
|
|
97
|
-
export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3
|
|
97
|
+
export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3,
|
|
98
|
+
/** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
|
|
99
|
+
shortSymbol = symbol) {
|
|
98
100
|
// Step 1: Get orderbooks from both sides
|
|
99
101
|
const [longOB, shortOB, longBal, shortBal] = await Promise.all([
|
|
100
102
|
longAdapter.getOrderbook(symbol),
|
|
101
|
-
shortAdapter.getOrderbook(
|
|
103
|
+
shortAdapter.getOrderbook(shortSymbol),
|
|
102
104
|
longAdapter.getBalance(),
|
|
103
105
|
shortAdapter.getBalance(),
|
|
104
106
|
]);
|
package/dist/dashboard/server.js
CHANGED
|
@@ -44,11 +44,11 @@ async function pollArbData() {
|
|
|
44
44
|
let exchangeStatus = {};
|
|
45
45
|
let exchangeErrors = {};
|
|
46
46
|
try {
|
|
47
|
-
const { fetchAllFundingRates } = await import("../funding/rates.js");
|
|
47
|
+
const { fetchAllFundingRates, isRankedComparison } = await import("../funding/rates.js");
|
|
48
48
|
const snapshot = await fetchAllFundingRates({ minSpread: 5 });
|
|
49
49
|
exchangeStatus = snapshot.exchangeStatus;
|
|
50
50
|
exchangeErrors = snapshot.exchangeErrors;
|
|
51
|
-
for (const sym of snapshot.symbols.slice(0, 20)) {
|
|
51
|
+
for (const sym of snapshot.symbols.filter(isRankedComparison).slice(0, 20)) {
|
|
52
52
|
opportunities.push({
|
|
53
53
|
symbol: sym.symbol,
|
|
54
54
|
longExchange: sym.longExchange,
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -235,6 +235,8 @@ function connect() {
|
|
|
235
235
|
// ── Helpers ──
|
|
236
236
|
function fmt(v, d=2) { const n=Number(v)||0; return n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); }
|
|
237
237
|
function usdOrDash(v, d=2) { return v==null ? '-' : '$'+fmt(v,d); }
|
|
238
|
+
// Market row cells: prices in the row's quote currency (QFEX SAMSUNG-KRW: "220,150.00 KRW", never $); OI is BASE units (never $); funding null = "-" (not 0%).
|
|
239
|
+
function marketCells(m) { const q=(typeof m.quoteAsset==='string'&&m.quoteAsset!=='USD')?escHtml(m.quoteAsset):null; const px=(v,d=2)=>v==null?'-':(q===null?'$'+fmt(v,d):fmt(v,d)+' '+q); const vol=m.volume24h==null&&typeof m.volume24hQuote==='string'?px(m.volume24hQuote,0):usdOrDash(m.volume24h,0); const base=escHtml(String(m.symbol).split(':').pop().replace(/-PERP$/i,'')); return { mark: px(m.markPrice), index: px(m.indexPrice), funding: m.fundingRate==null?'-':(Number(m.fundingRate)*100).toFixed(4)+'%', volume: vol, oi: m.openInterest==null?'-':Number(m.openInterest).toLocaleString('en-US',{maximumFractionDigits:4})+' '+base }; }
|
|
238
240
|
function pnlClass(v) { return Number(v)>=0?'pnl-pos':'pnl-neg'; }
|
|
239
241
|
function pnlSign(v) { const n=Number(v)||0; return n>=0?'+$'+fmt(Math.abs(n)):'-$'+fmt(Math.abs(n)); }
|
|
240
242
|
function rateClass(v) { const n=Number(v)||0; return n>0?'rate-positive':n<0?'rate-negative':'rate-neutral'; }
|
|
@@ -243,7 +245,8 @@ function spreadClass(v) { return v>=50?'spread-high':v>=20?'spread-mid':'spread-
|
|
|
243
245
|
const NA = '<span style="color:var(--yellow,#e5c07b)">unavailable</span>';
|
|
244
246
|
function usdOrNA(v, d=2) { return v==null ? NA : '$'+fmt(v,d); }
|
|
245
247
|
function pnlOrNA(v) { return v==null ? NA : pnlSign(v); }
|
|
246
|
-
|
|
248
|
+
// Every venue-supplied string (symbol, quoteAsset, dex, exchange, order fields) goes through this before innerHTML.
|
|
249
|
+
function escHtml(v) { return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]); }
|
|
247
250
|
|
|
248
251
|
// ── Portfolio Rendering ──
|
|
249
252
|
function renderTotals(t) {
|
|
@@ -325,16 +328,16 @@ function renderPanels(exchanges) {
|
|
|
325
328
|
const notional = p.markPrice == null ? null : Math.abs(Number(p.size)) * Number(p.markPrice);
|
|
326
329
|
const margin = notional == null ? null : p.leverage > 0 ? notional / p.leverage : notional;
|
|
327
330
|
const roe = margin != null && margin > 0 && p.unrealizedPnl != null ? (Number(p.unrealizedPnl) / margin * 100) : null;
|
|
328
|
-
return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
|
|
331
|
+
return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(p.side)}">\${escHtml(String(p.side).toUpperCase())}</td><td>\${escHtml(p.size)}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
|
|
329
332
|
}).join('')}</tbody></table>\`:(posKnown?'<div class="empty-msg">No open positions</div>':'<div class="empty-msg">Positions '+NA+'</div>')}
|
|
330
333
|
<div class="section-title">Open Orders (\${ordKnown ? filteredOrders.length : '?'})</div>
|
|
331
334
|
\${filteredOrders.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Type</th><th>Price</th><th>Size</th><th>Filled</th><th>Status</th></tr></thead><tbody>\${filteredOrders.map(o=>{
|
|
332
335
|
const dex = getDex(o.symbol);
|
|
333
336
|
const sym = o.symbol.includes(':') ? o.symbol.split(':').slice(1).join(':') : o.symbol;
|
|
334
|
-
return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${o.side}">\${o.side.toUpperCase()}</td><td>\${o.type}</td><td>$\${fmt(o.price)}</td><td>\${o.size}</td><td>\${o.filled}</td><td>\${o.status}</td></tr>\`;
|
|
337
|
+
return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(o.side)}">\${escHtml(String(o.side).toUpperCase())}</td><td>\${escHtml(o.type)}</td><td>$\${fmt(o.price)}</td><td>\${escHtml(o.size)}</td><td>\${escHtml(o.filled)}</td><td>\${escHtml(o.status)}</td></tr>\`;
|
|
335
338
|
}).join('')}</tbody></table>\`:(ordKnown?'<div class="empty-msg">No open orders</div>':'<div class="empty-msg">Open orders '+NA+'</div>')}
|
|
336
339
|
<div class="section-title">Markets (Top 10)</div>
|
|
337
|
-
\${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td
|
|
340
|
+
\${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);const c=marketCells(m);return\`<tr><td>\${escHtml(m.symbol)}</td><td>\${c.mark}</td><td>\${c.index}</td><td class="\${rateClass(fr)}">\${c.funding}</td><td>\${c.volume}</td><td>\${c.oi}</td><td>\${m.maxLeverage==null?'-':m.maxLeverage+'x'}</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
|
|
338
341
|
</div>\`;
|
|
339
342
|
}).join('');
|
|
340
343
|
|
|
@@ -419,7 +422,7 @@ function renderArb() {
|
|
|
419
422
|
const barColor = o.spreadAnnual>=50?'var(--green)':o.spreadAnnual>=20?'var(--yellow)':'var(--muted)';
|
|
420
423
|
const bestMark = o.rates.reduce((best,r) => r.markPrice>0?r.markPrice:best, 0);
|
|
421
424
|
return \`<tr>
|
|
422
|
-
<td style="font-weight:600">\${o.symbol}</td>
|
|
425
|
+
<td style="font-weight:600">\${escHtml(o.symbol)}</td>
|
|
423
426
|
<td>
|
|
424
427
|
<div class="\${spreadClass(o.spreadAnnual)}">\${fmt(o.spreadAnnual,1)}%</div>
|
|
425
428
|
<div class="spread-bar"><div class="spread-bar-fill" style="width:\${barW}%;background:\${barColor}"></div></div>
|
|
@@ -514,7 +517,7 @@ function renderEvents() {
|
|
|
514
517
|
if (!events.length) { el.innerHTML='<div class="empty-msg">Waiting for events...</div>'; return; }
|
|
515
518
|
el.innerHTML = events.map(e => {
|
|
516
519
|
const cls = e.isCrit?'crit':e.isWarn?'warn':'';
|
|
517
|
-
return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${e.exchange}</span> <span style="color:var(--muted)">\${JSON.stringify(e.data).slice(0,120)}</span></div>\`;
|
|
520
|
+
return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${escHtml(e.exchange)}</span> <span style="color:var(--muted)">\${escHtml(JSON.stringify(e.data).slice(0,120))}</span></div>\`;
|
|
518
521
|
}).join('');
|
|
519
522
|
}
|
|
520
523
|
|
package/dist/errors.d.ts
CHANGED
|
@@ -320,3 +320,8 @@ export declare function unknownActionRemediation(venue: string, action: string):
|
|
|
320
320
|
* and "1e3" are refused). Returns the trimmed string.
|
|
321
321
|
*/
|
|
322
322
|
export declare function parsePositiveDecimal(raw: string, what: string): string;
|
|
323
|
+
/**
|
|
324
|
+
* A keyless account read (positions, balance, orders, history): the one typed refusal every venue
|
|
325
|
+
* returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
|
|
326
|
+
*/
|
|
327
|
+
export declare function noAccountKeyError(venue: string): PerpError;
|
package/dist/errors.js
CHANGED
|
@@ -388,3 +388,13 @@ export function parsePositiveDecimal(raw, what) {
|
|
|
388
388
|
}
|
|
389
389
|
return s;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* A keyless account read (positions, balance, orders, history): the one typed refusal every venue
|
|
393
|
+
* returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
|
|
394
|
+
*/
|
|
395
|
+
export function noAccountKeyError(venue) {
|
|
396
|
+
return new PerpError("NO_SIGNER_AVAILABLE", `${venue}: no account key configured — account data unavailable`, {
|
|
397
|
+
exchange: venue,
|
|
398
|
+
remediation: `Set up an account for ${venue}: perp setup (or perp -e ${venue} wallet show to check)`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type AddressKind = "evm" | "solana";
|
|
2
|
+
export declare function isEvmAddress(value: string): boolean;
|
|
3
|
+
/** A base58 string that decodes to a 32-byte public key. */
|
|
4
|
+
export declare function isSolanaAddress(value: string): boolean;
|
|
5
|
+
/** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
|
|
6
|
+
export declare function requireSolanaAddress(value: string, field?: string): string;
|
|
7
|
+
/** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
|
|
8
|
+
export declare function requireAddressOfKind(venue: string, kind: AddressKind, address: string): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public account address formats (keyless reads by address): EVM `0x` + 40 hex (Hyperliquid,
|
|
3
|
+
* Lighter) and base58 Solana public keys (Pacifica). A wrong format is refused with
|
|
4
|
+
* INVALID_PARAMS before any request — the venues answer a foreign address with an empty success
|
|
5
|
+
* (Pacifica `success: true, []`), which would read as "no positions".
|
|
6
|
+
*/
|
|
7
|
+
import bs58 from "bs58";
|
|
8
|
+
import { PerpError } from "../errors.js";
|
|
9
|
+
const EVM_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
10
|
+
export function isEvmAddress(value) {
|
|
11
|
+
return EVM_RE.test(value.trim());
|
|
12
|
+
}
|
|
13
|
+
/** A base58 string that decodes to a 32-byte public key. */
|
|
14
|
+
export function isSolanaAddress(value) {
|
|
15
|
+
try {
|
|
16
|
+
return bs58.decode(value.trim()).length === 32;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
|
|
23
|
+
export function requireSolanaAddress(value, field = "address") {
|
|
24
|
+
const a = value.trim();
|
|
25
|
+
if (!isSolanaAddress(a)) {
|
|
26
|
+
throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) ${field}: "${value}"${isEvmAddress(a) ? " — that is an EVM address; Pacifica accounts are Solana addresses" : ""}`, {
|
|
27
|
+
exchange: "pacifica", remediation: "Pass the base58 Solana address you use on Pacifica",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return a;
|
|
31
|
+
}
|
|
32
|
+
/** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
|
|
33
|
+
export function requireAddressOfKind(venue, kind, address) {
|
|
34
|
+
const a = address.trim();
|
|
35
|
+
if (kind === "evm") {
|
|
36
|
+
if (isEvmAddress(a))
|
|
37
|
+
return a;
|
|
38
|
+
throw new PerpError("INVALID_PARAMS", `${venue} expects an EVM 0x address (0x + 40 hex)${isSolanaAddress(a) ? " — that is a Solana address" : ""}: "${address}"`, {
|
|
39
|
+
exchange: venue, remediation: `Pass the 0x address of the ${venue} account`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (isSolanaAddress(a))
|
|
43
|
+
return a;
|
|
44
|
+
throw new PerpError("INVALID_PARAMS", `${venue} expects a base58 Solana address${isEvmAddress(a) ? " — that is an EVM address" : ""}: "${address}"`, {
|
|
45
|
+
exchange: venue, remediation: `Pass the base58 Solana address of the ${venue} account`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* an `entryTpsl` bundle.
|
|
23
23
|
*/
|
|
24
24
|
import type { EvmSigner } from "../../signer/interface.js";
|
|
25
|
-
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
|
|
25
|
+
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
|
|
26
26
|
import type { ResolvedSlippage } from "../slippage.js";
|
|
27
27
|
import { type TradingV2 } from "../trading-v2.js";
|
|
28
28
|
import type { VenueCreateContext, VenueDefinition } from "../venue.js";
|
|
@@ -37,6 +37,8 @@ export declare const ARCUS_GOOD_TIL_HORIZON_MS: number;
|
|
|
37
37
|
export declare const ARCUS_MAX_PROTECTION_DEVIATION = 0.1;
|
|
38
38
|
/** clientId max (36) minus the "-tp"/"-sl" suffix of entryTpsl child legs. */
|
|
39
39
|
export declare const ARCUS_TPSL_CLIENT_ID_MAX = 33;
|
|
40
|
+
/** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
|
|
41
|
+
export declare const ARCUS_HISTORY_CAP = 1000;
|
|
40
42
|
export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
|
|
41
43
|
readonly name: "arcus";
|
|
42
44
|
readonly venue: VenueDefinition;
|
|
@@ -57,6 +59,12 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
57
59
|
private accountQuery;
|
|
58
60
|
private loadMarkets;
|
|
59
61
|
private market;
|
|
62
|
+
/**
|
|
63
|
+
* A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
|
|
64
|
+
* is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
|
|
65
|
+
* the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
|
|
66
|
+
*/
|
|
67
|
+
private onlineMarket;
|
|
60
68
|
private symbolOf;
|
|
61
69
|
/** Protection price: slippage from `ref`, aligned toward crossing on the tier tick, ≤ 10 % from `ref`. */
|
|
62
70
|
private protectionPrice;
|
|
@@ -144,11 +152,7 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
144
152
|
asks: [string, string][];
|
|
145
153
|
}>;
|
|
146
154
|
getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
|
|
147
|
-
getFundingHistory(symbol: string, limit?: number): Promise<
|
|
148
|
-
time: number;
|
|
149
|
-
rate: string;
|
|
150
|
-
price: string | null;
|
|
151
|
-
}[]>;
|
|
155
|
+
getFundingHistory(symbol: string, limit?: number): Promise<FundingHistoryRow[]>;
|
|
152
156
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
153
157
|
private rawPositions;
|
|
154
158
|
getBalance(): Promise<ExchangeBalance>;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* an `entryTpsl` bundle.
|
|
23
23
|
*/
|
|
24
24
|
import { PerpError } from "../../errors.js";
|
|
25
|
+
import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
|
|
25
26
|
import { OwsEvmSigner } from "../../signer/ows-evm.js";
|
|
26
27
|
import { OwsSolanaSigner } from "../../signer/ows-solana.js";
|
|
27
28
|
import { alignProtectionPrice, floorSize, mulDecimal, requirePositive, slippagePrice, subDecimal, toUnitsExact, unitsToString } from "../precision.js";
|
|
@@ -90,6 +91,15 @@ function finalityOf(s) {
|
|
|
90
91
|
return s === "filled" || s === "rejected" || s === "cancelled" ? "final" : "pending";
|
|
91
92
|
}
|
|
92
93
|
const usToMs = (us) => Math.floor(us / 1000);
|
|
94
|
+
/** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
|
|
95
|
+
export const ARCUS_HISTORY_CAP = 1000;
|
|
96
|
+
function arcusLimit(what, limit) {
|
|
97
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > ARCUS_HISTORY_CAP) {
|
|
98
|
+
throw new PerpError("INVALID_PARAMS", `Arcus: ${what} limit must be 1..${ARCUS_HISTORY_CAP} (got ${limit})`, {
|
|
99
|
+
remediation: `Arcus serves at most ${ARCUS_HISTORY_CAP} rows per request`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
93
103
|
function sideLower(s) { return s === "BUY" ? "buy" : "sell"; }
|
|
94
104
|
function tifFromRead(t) {
|
|
95
105
|
// Reads report resting (GTT) orders as the legacy "GTC".
|
|
@@ -281,6 +291,21 @@ export class ArcusAdapter {
|
|
|
281
291
|
}
|
|
282
292
|
return m;
|
|
283
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
|
|
296
|
+
* is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
|
|
297
|
+
* the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
|
|
298
|
+
*/
|
|
299
|
+
async onlineMarket(symbol) {
|
|
300
|
+
const m = await this.market(symbol);
|
|
301
|
+
if (!isArcusOnlineMarket(m)) {
|
|
302
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Arcus: market ${m.marketDisplayName} is ${m.status} (not tradable; no live market data)`, {
|
|
303
|
+
exchange: "arcus", status: m.status, tradable: false,
|
|
304
|
+
remediation: "List live markets with: perp market list -e arcus",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return m;
|
|
308
|
+
}
|
|
284
309
|
async symbolOf(marketId) {
|
|
285
310
|
let m = (await this.loadMarkets(false)).byId.get(marketId);
|
|
286
311
|
if (!m)
|
|
@@ -878,20 +903,22 @@ export class ArcusAdapter {
|
|
|
878
903
|
return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
|
|
879
904
|
}
|
|
880
905
|
async getOrderbook(symbol) {
|
|
881
|
-
const m = await this.
|
|
906
|
+
const m = await this.onlineMarket(symbol);
|
|
882
907
|
const ob = await this.client.get(`/v1/l2OrderBook/${encodeURIComponent(m.marketDisplayName)}`, "l2OrderBook", { nLevels: ORDERBOOK_LEVELS });
|
|
883
908
|
return { bids: ob.bids, asks: ob.asks };
|
|
884
909
|
}
|
|
885
910
|
async getRecentTrades(symbol, limit = 50) {
|
|
886
|
-
|
|
911
|
+
arcusLimit("trades", limit);
|
|
912
|
+
const m = await this.onlineMarket(symbol);
|
|
887
913
|
const res = await this.client.get("/v1/trades", "trades", { market: m.marketDisplayName, limit });
|
|
888
|
-
// Public tape
|
|
914
|
+
// Public tape: no fee applies to the viewer → null, never "0".
|
|
889
915
|
return res.trades.map(t => ({ time: usToMs(t.timestamp), symbol: m.baseAsset, side: sideLower(t.side), price: t.price, size: t.size, fee: null }));
|
|
890
916
|
}
|
|
891
917
|
async getFundingHistory(symbol, limit = 100) {
|
|
892
|
-
|
|
918
|
+
arcusLimit("funding history", limit);
|
|
919
|
+
const m = await this.onlineMarket(symbol);
|
|
893
920
|
const res = await this.client.get("/v1/fundingRates", "fundingRates", { market: m.marketDisplayName, limit });
|
|
894
|
-
return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null }));
|
|
921
|
+
return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null, intervalHours: 1, rateKind: "lastSettled" }));
|
|
895
922
|
}
|
|
896
923
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
897
924
|
if (!CANDLE_TIMEFRAMES.has(interval)) {
|
|
@@ -899,7 +926,7 @@ export class ArcusAdapter {
|
|
|
899
926
|
remediation: `Use one of: ${[...CANDLE_TIMEFRAMES].join(", ")}`,
|
|
900
927
|
});
|
|
901
928
|
}
|
|
902
|
-
const m = await this.
|
|
929
|
+
const m = await this.onlineMarket(symbol);
|
|
903
930
|
const span = CANDLE_WINDOW_BARS * CANDLE_MS[interval];
|
|
904
931
|
const windows = Math.max(1, Math.ceil((endTime - startTime) / span));
|
|
905
932
|
if (windows > CANDLE_MAX_WINDOWS) {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* - tradable: status === "ONLINE".
|
|
25
25
|
*/
|
|
26
26
|
import { PerpError } from "../../errors.js";
|
|
27
|
+
import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
|
|
27
28
|
import { canonicalFor } from "../canonical-symbols.js";
|
|
28
29
|
import { arcusVenue } from "../venues/arcus.js";
|
|
29
30
|
import { ArcusClient } from "./client.js";
|
|
@@ -101,7 +102,7 @@ export function toArcusVenueMarket(m) {
|
|
|
101
102
|
maxLeverage: maxLeverageOf(m),
|
|
102
103
|
marginTiers: null,
|
|
103
104
|
session: sessionOf(m),
|
|
104
|
-
tradable: m
|
|
105
|
+
tradable: isArcusOnlineMarket(m),
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
@@ -110,7 +111,7 @@ export function toArcusVenueMarket(m) {
|
|
|
110
111
|
* market / funding lists rather than listed at a $0 price and 0 % funding.
|
|
111
112
|
*/
|
|
112
113
|
export function onlineArcusMarkets(raw) {
|
|
113
|
-
return raw.filter(
|
|
114
|
+
return raw.filter(isArcusOnlineMarket);
|
|
114
115
|
}
|
|
115
116
|
async function fetchMarkets(net) {
|
|
116
117
|
const raw = await fetchArcusRawMarkets(new ArcusClient(net, null));
|