perp-cli 0.14.2 → 0.15.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.md +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +168 -36
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ui.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +9 -4
- package/dist/exchanges/aster.js +125 -55
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -12,7 +12,7 @@ import { registerStrategy } from "../strategy-registry.js";
|
|
|
12
12
|
import { getFundingHours } from "../../funding.js";
|
|
13
13
|
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
14
14
|
import { pruneExecutionLog } from "../../execution-log.js";
|
|
15
|
-
import { buildAdapterMap, getPerpSymbol, findRate, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
|
|
15
|
+
import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
|
|
16
16
|
import { SpotPerpExecutor } from "./spot-perp-executor.js";
|
|
17
17
|
import { findSpotQuoteBalance, getBotSpotAdapter, nonQuoteSpotBalances, spotNetworkOf, spotSharesPerpMargin, spotTokenBase } from "../../exchanges/spot-factory.js";
|
|
18
18
|
import { getVenue } from "../../exchanges/venue.js";
|
|
@@ -285,7 +285,7 @@ export class SpotPerpArbStrategy {
|
|
|
285
285
|
ctx.state.set("spaPositions", remaining);
|
|
286
286
|
ctx.state.set("arbPositions", remaining.length);
|
|
287
287
|
}
|
|
288
|
-
// ── Rotation check (only to perp venues that match the spot leg
|
|
288
|
+
// ── Rotation check (only to perp venues that match the spot leg) ──
|
|
289
289
|
const perpMarketsCache = new Map();
|
|
290
290
|
const allowedBySpotVenue = new Map();
|
|
291
291
|
const currentPositions = ctx.state.get("spaPositions");
|
|
@@ -313,7 +313,7 @@ export class SpotPerpArbStrategy {
|
|
|
313
313
|
const allowed = allowedByBase.get(pos.symbol.toUpperCase());
|
|
314
314
|
if (allowed === undefined)
|
|
315
315
|
continue;
|
|
316
|
-
const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange, allowed);
|
|
316
|
+
const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange, allowed, ctx.log);
|
|
317
317
|
const best = candidates[0];
|
|
318
318
|
if (!best || best.exchange === pos.perpExchange)
|
|
319
319
|
continue;
|
|
@@ -331,12 +331,12 @@ export class SpotPerpArbStrategy {
|
|
|
331
331
|
continue;
|
|
332
332
|
const perpSym = getPerpSymbol(pos.symbol, pos.perpExchange);
|
|
333
333
|
const newMaxLev = findRate(ratesByExchange, best.exchange, getPerpSymbol(pos.symbol, best.exchange))?.maxLeverage
|
|
334
|
-
?? findRate(ratesByExchange, best.exchange, pos.symbol)?.maxLeverage
|
|
334
|
+
?? findRate(ratesByExchange, best.exchange, pos.symbol)?.maxLeverage;
|
|
335
335
|
const ok = await this.executor.rotateShort({
|
|
336
336
|
symbol: pos.symbol, perpSymbol: perpSym,
|
|
337
337
|
currentExchange: pos.perpExchange, newExchange: best.exchange,
|
|
338
338
|
currentAdapter: oldAdapter, newAdapter,
|
|
339
|
-
size: pos.size, leverage:
|
|
339
|
+
size: pos.size, leverage: cappedLeverage(p.leverage, [{ exchange: best.exchange, maxLeverage: newMaxLev }], ctx.log), log: ctx.log,
|
|
340
340
|
});
|
|
341
341
|
if (ok.ok) {
|
|
342
342
|
// The spot leg is unchanged: the state entry follows the short to its new venue.
|
|
@@ -375,7 +375,7 @@ export class SpotPerpArbStrategy {
|
|
|
375
375
|
const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
|
|
376
376
|
if (!spotAdapter)
|
|
377
377
|
continue;
|
|
378
|
-
// Canonical symbol + price guard
|
|
378
|
+
// Canonical symbol + price guard: base → perp venues it may hedge on.
|
|
379
379
|
const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
|
|
380
380
|
for (const [base, allowed] of allowedByBase) {
|
|
381
381
|
// HL spot: skip tokens not in whitelist
|
|
@@ -387,7 +387,7 @@ export class SpotPerpArbStrategy {
|
|
|
387
387
|
continue;
|
|
388
388
|
if (this.quarantined(base, ctx))
|
|
389
389
|
continue;
|
|
390
|
-
const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange, allowed);
|
|
390
|
+
const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange, allowed, ctx.log);
|
|
391
391
|
for (const cand of candidates) {
|
|
392
392
|
if (cand.score.annualized < p.min_rate || cand.score.consistency < p.min_consistency)
|
|
393
393
|
continue;
|
|
@@ -501,7 +501,7 @@ export class SpotPerpArbStrategy {
|
|
|
501
501
|
return byBase;
|
|
502
502
|
}
|
|
503
503
|
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
|
|
504
|
-
async findPerpCandidates(symbol, adapters, ratesByExchange, allowedPerpVenues) {
|
|
504
|
+
async findPerpCandidates(symbol, adapters, ratesByExchange, allowedPerpVenues, log) {
|
|
505
505
|
const candidates = [];
|
|
506
506
|
for (const [exName, adapter] of adapters) {
|
|
507
507
|
if (!allowedPerpVenues.has(exName))
|
|
@@ -510,8 +510,20 @@ export class SpotPerpArbStrategy {
|
|
|
510
510
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
511
511
|
if (!ri || ri.rate <= 0)
|
|
512
512
|
continue;
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
// A failed history read is not "too little history": say so, then try the next symbol form;
|
|
514
|
+
// a venue with no readable form is left out this cycle.
|
|
515
|
+
let score = null;
|
|
516
|
+
for (const sym of perpSym === symbol ? [perpSym] : [perpSym, symbol]) {
|
|
517
|
+
try {
|
|
518
|
+
score = await this.scoreFundingLocal(adapter, sym, exName);
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
log(` [SPA] ${sym} on ${exName}: funding history unreadable — not a candidate this cycle: ${e instanceof Error ? e.message : String(e)}`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (score)
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
515
527
|
if (!score)
|
|
516
528
|
continue;
|
|
517
529
|
candidates.push({ exchange: exName, rate: ri.rate, score });
|
|
@@ -569,8 +581,7 @@ export class SpotPerpArbStrategy {
|
|
|
569
581
|
const perpSymbol = getPerpSymbol(symbol, perpExchange);
|
|
570
582
|
const rateInfo = findRate(ratesByExchange, perpExchange, perpSymbol)
|
|
571
583
|
?? findRate(ratesByExchange, perpExchange, symbol);
|
|
572
|
-
const
|
|
573
|
-
const leverage = Math.min(p.leverage, perpMaxLev);
|
|
584
|
+
const leverage = cappedLeverage(p.leverage, [{ exchange: perpExchange, maxLeverage: rateInfo?.maxLeverage }], ctx.log);
|
|
574
585
|
// Pre-entry: verify fundingHours (aster lazy bootstrap may not have run yet)
|
|
575
586
|
if (perpExchange === "aster" && rateInfo && "getFundingHours" in perpAdapter) {
|
|
576
587
|
const actualFh = await perpAdapter.getFundingHours(symbol);
|
|
@@ -26,7 +26,7 @@ export type ExecutionResult = {
|
|
|
26
26
|
/**
|
|
27
27
|
* closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
|
|
28
28
|
* manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
|
|
29
|
-
* the position and never retry it automatically
|
|
29
|
+
* the position and never retry it automatically.
|
|
30
30
|
*/
|
|
31
31
|
export type CloseResult = {
|
|
32
32
|
closed: true;
|
|
@@ -140,7 +140,7 @@ export class SpotPerpExecutor {
|
|
|
140
140
|
await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
|
|
141
141
|
return { success: false, error: `pre-trade balance: ${msg}` };
|
|
142
142
|
}
|
|
143
|
-
// Step 1: Buy spot — the hedge is sized to what actually filled
|
|
143
|
+
// Step 1: Buy spot — the hedge is sized to what actually filled
|
|
144
144
|
let fill;
|
|
145
145
|
try {
|
|
146
146
|
fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
|
|
@@ -10,7 +10,8 @@ export interface EnrichedSnapshot extends MarketSnapshot {
|
|
|
10
10
|
bids: [string, string][];
|
|
11
11
|
asks: [string, string][];
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/** Base-asset open interest; null = not read / not published (never "0" as a stand-in). */
|
|
14
|
+
openInterest: string | null;
|
|
14
15
|
}
|
|
15
16
|
export type StrategyAction = {
|
|
16
17
|
type: "place_order";
|
|
@@ -78,7 +78,7 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
78
78
|
const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
|
|
79
79
|
const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
|
|
80
80
|
const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
|
|
81
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest ?? "-" })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
82
82
|
const upnl = parseFloat(p.unrealizedPnl);
|
|
83
83
|
return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${upnl.toFixed(2)}` })] }, i));
|
|
84
84
|
})] }))] }), openOrdersError !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Open orders unavailable (", openOrdersError, ")"] }) })) : (_jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
|
package/dist/cache.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
10
11
|
const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
|
|
11
12
|
// TTL presets
|
|
12
13
|
export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
|
|
@@ -41,16 +42,21 @@ export function getCached(key) {
|
|
|
41
42
|
}
|
|
42
43
|
export function setCached(key, data, ttlMs) {
|
|
43
44
|
ensureDir();
|
|
45
|
+
const file = join(CACHE_DIR, `${safeKey(key)}.json`);
|
|
46
|
+
// Unique per writer: concurrent processes writing the same key must not share one tmp file
|
|
47
|
+
// (a rename of another writer's half-written tmp would publish a partial entry).
|
|
48
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
44
49
|
try {
|
|
45
50
|
const entry = { ts: Date.now(), ttl: ttlMs, data };
|
|
46
|
-
|
|
47
|
-
const file = join(CACHE_DIR, `${safeKey(key)}.json`);
|
|
48
|
-
const tmp = file + ".tmp";
|
|
49
|
-
writeFileSync(tmp, content);
|
|
51
|
+
writeFileSync(tmp, JSON.stringify(entry));
|
|
50
52
|
renameSync(tmp, file);
|
|
51
53
|
}
|
|
52
54
|
catch {
|
|
53
|
-
// cache write failure is non-fatal
|
|
55
|
+
// cache write failure is non-fatal; never leave this writer's tmp file behind
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(tmp);
|
|
58
|
+
}
|
|
59
|
+
catch { /* not written */ }
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
@@ -94,7 +100,7 @@ export function invalidateCache(keyPrefix) {
|
|
|
94
100
|
const files = readdirSync(CACHE_DIR);
|
|
95
101
|
for (const f of files) {
|
|
96
102
|
if (!f.endsWith(".json"))
|
|
97
|
-
continue;
|
|
103
|
+
continue; // also skips in-flight "<key>.json.<pid>.<rand>.tmp" files
|
|
98
104
|
if (!prefix || f.startsWith(prefix)) {
|
|
99
105
|
try {
|
|
100
106
|
unlinkSync(join(CACHE_DIR, f));
|
package/dist/cli-program.d.ts
CHANGED
|
@@ -28,8 +28,23 @@ export declare function landingData(program: Command, version: string): {
|
|
|
28
28
|
name: string;
|
|
29
29
|
description: string;
|
|
30
30
|
}[];
|
|
31
|
+
venues: {
|
|
32
|
+
id: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
roles: string[];
|
|
35
|
+
apiStatus: "official" | "unofficial";
|
|
36
|
+
note: string | null;
|
|
37
|
+
}[];
|
|
31
38
|
};
|
|
32
39
|
export declare function buildProgram(opts: {
|
|
33
40
|
defaultExchange: string;
|
|
34
41
|
version: string;
|
|
35
42
|
}): CliProgram;
|
|
43
|
+
/**
|
|
44
|
+
* The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
|
|
45
|
+
* `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
|
|
46
|
+
* (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
|
|
47
|
+
* before the dry-run guard and before any action, so a dry-run plan can never present a gated
|
|
48
|
+
* venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assertCommandVenueRole(actionCommand: Command, exchangeOpt: string): void;
|
package/dist/cli-program.js
CHANGED
|
@@ -6,13 +6,15 @@ import { Command } from "commander";
|
|
|
6
6
|
import { cliCredentialOptsFrom, hasCredentials, resolveCredentials } from "./config.js";
|
|
7
7
|
import { PacificaAdapter } from "./exchanges/pacifica.js";
|
|
8
8
|
import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
|
|
9
|
-
import { getVenue, listVenues, resolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
|
|
9
|
+
import { getVenue, listVenues, resolveVenueId, roleUnsupported, tryResolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
|
|
10
10
|
import { registerAdapterNetwork } from "./exchanges/adapter-network.js";
|
|
11
11
|
import "./exchanges/venues/index.js";
|
|
12
12
|
import { PerpError } from "./errors.js";
|
|
13
13
|
import { registerMarketCommands } from "./commands/market.js";
|
|
14
14
|
import { registerAccountCommands } from "./commands/account.js";
|
|
15
15
|
import { registerOutcomeCommands } from "./commands/outcome.js";
|
|
16
|
+
import { registerOptionsCommands } from "./commands/options.js";
|
|
17
|
+
import { registerPacificaEarnCommands } from "./commands/pacifica-earn.js";
|
|
16
18
|
import { registerTradeCommands } from "./commands/trade.js";
|
|
17
19
|
// manage commands now register under `wallet manage ...` via registerWalletCommands.
|
|
18
20
|
// stream commands removed — WS feeds still used by dashboard/event-stream internally
|
|
@@ -86,6 +88,11 @@ export function landingData(program, version) {
|
|
|
86
88
|
return {
|
|
87
89
|
name: program.name(),
|
|
88
90
|
version,
|
|
91
|
+
// Registry facts only (no reads): an "unofficial" venue is read through an undocumented API.
|
|
92
|
+
venues: listVenues().map(v => ({
|
|
93
|
+
id: v.id, displayName: v.displayName, roles: [...v.roles], apiStatus: v.apiStatus.kind,
|
|
94
|
+
note: v.apiStatus.kind === "unofficial" ? v.apiStatus.note : null,
|
|
95
|
+
})),
|
|
89
96
|
usage: `${program.name()} [options] <command> [args] (add --json for one JSON document on stdout)`,
|
|
90
97
|
help: `${program.name()} <command> --help`,
|
|
91
98
|
commands: program.commands
|
|
@@ -131,9 +138,9 @@ export function buildProgram(opts) {
|
|
|
131
138
|
});
|
|
132
139
|
let _adapter = null;
|
|
133
140
|
/**
|
|
134
|
-
* One adapter per `${venueId}:${network}:${wallet}:${hlDex}
|
|
141
|
+
* One adapter per `${venueId}:${network}:${wallet}:${hlDex}`.
|
|
135
142
|
* Every adapter is built by the venue definition's create(ctx) — the single
|
|
136
|
-
* creation path shared with the MCP server
|
|
143
|
+
* creation path shared with the MCP server.
|
|
137
144
|
*/
|
|
138
145
|
const _adapters = new Map();
|
|
139
146
|
function cliCredentialOpts() {
|
|
@@ -147,7 +154,7 @@ export function buildProgram(opts) {
|
|
|
147
154
|
const wallet = opts.ows ? `ows:${opts.ows}` : (opts.wallet ?? "default");
|
|
148
155
|
return `${venue}:${network}:${wallet}:${hlDex ?? "-"}`;
|
|
149
156
|
}
|
|
150
|
-
/** restrictedRegions notice: stderr, once per venue per process (
|
|
157
|
+
/** restrictedRegions notice: stderr, once per venue per process (warn, never block). */
|
|
151
158
|
const _restrictedWarned = new Set();
|
|
152
159
|
function warnRestrictedRegions(def) {
|
|
153
160
|
if (def.restrictedRegions.length === 0 || _restrictedWarned.has(def.id))
|
|
@@ -167,6 +174,10 @@ export function buildProgram(opts) {
|
|
|
167
174
|
remediation: `${def.id} is market-data only — use the funding/market public commands (e.g. perp arb scan)`,
|
|
168
175
|
});
|
|
169
176
|
}
|
|
177
|
+
// A market-data-only venue (TxFlow: unofficial read-only API) never gets a credentialed adapter:
|
|
178
|
+
// every account / trading / funds / bot path refuses here, before any credential or request.
|
|
179
|
+
if (!publicOnly && !def.roles.includes("account"))
|
|
180
|
+
throw roleUnsupported(def, "account");
|
|
170
181
|
const network = currentNetwork();
|
|
171
182
|
const hlDex = def.id === "hyperliquid" ? dex : null;
|
|
172
183
|
const key = publicOnly ? `${def.id}:${network}:public:${hlDex ?? "-"}` : adapterCacheKey(def.id, network, hlDex);
|
|
@@ -246,6 +257,8 @@ export function buildProgram(opts) {
|
|
|
246
257
|
registerMarketCommands(program, getPublicAdapter, isJson, getPublicAdapterForExchange, getAdapter);
|
|
247
258
|
registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
248
259
|
registerOutcomeCommands(program, getAdapterForExchange, isJson);
|
|
260
|
+
registerOptionsCommands(program, isJson);
|
|
261
|
+
registerPacificaEarnCommands(program, getAdapterForExchange, isJson);
|
|
249
262
|
registerTradeCommands(program, getAdapter, isJson, isDryRunFlag, getAdapterForExchange);
|
|
250
263
|
// manage tree wired inside registerWalletCommands below.
|
|
251
264
|
// stream commands removed
|
|
@@ -273,7 +286,15 @@ export function buildProgram(opts) {
|
|
|
273
286
|
// registerFundsCommands above.
|
|
274
287
|
// Background processes & strategies
|
|
275
288
|
registerBackgroundCommands(program, isJson, getAdapterForExchange);
|
|
276
|
-
|
|
289
|
+
// Every strategy command (bots, plans, TWAP / DCA / grid, `strategy run`, `--exchanges` legs,
|
|
290
|
+
// config-file venues) builds its adapters through these: a venue without the trading role
|
|
291
|
+
// (TxFlow, release-gated RISEx) is refused here, before any key is opened — also under --dry-run.
|
|
292
|
+
const strategyVenueGate = (venue) => {
|
|
293
|
+
const def = getVenue(venue);
|
|
294
|
+
if (!def.roles.includes("trading"))
|
|
295
|
+
throw roleUnsupported(def, "automation");
|
|
296
|
+
};
|
|
297
|
+
registerStrategyCommands(program, async () => { strategyVenueGate(getExchange()); return getAdapter(); }, async (exchange) => { strategyVenueGate(exchange); return getAdapterForExchange(exchange); }, isJson);
|
|
277
298
|
// Agent-friendly commands
|
|
278
299
|
registerRiskCommands(program, getAdapterForExchange, isJson);
|
|
279
300
|
registerHistoryCommands(program, isJson, getAdapterForExchange);
|
|
@@ -286,7 +307,12 @@ export function buildProgram(opts) {
|
|
|
286
307
|
registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex);
|
|
287
308
|
registerInitCommand(program);
|
|
288
309
|
registerAlertCommands(program, isJson);
|
|
310
|
+
installNestedHelp(program);
|
|
311
|
+
installMissingSubcommandError(program);
|
|
289
312
|
// Switch shared API URLs if --network testnet is used
|
|
313
|
+
// A venue without the trading role (market-data-only TxFlow, release-gated RISEx) refuses every
|
|
314
|
+
// trading command before anything else — also under --dry-run, whose plans skip the adapter.
|
|
315
|
+
program.hook("preAction", (_root, actionCommand) => assertCommandVenueRole(actionCommand, program.opts().exchange));
|
|
290
316
|
// Validates -n and enforces default-deny --dry-run before any action runs.
|
|
291
317
|
installDryRunGuard(program);
|
|
292
318
|
// Secrets on argv (review L10): -k is rejected outright; --passphrase still works but warns loudly.
|
|
@@ -301,3 +327,97 @@ export function buildProgram(opts) {
|
|
|
301
327
|
program.hook("preAction", () => setQuarantineNetwork(networkOf(program)));
|
|
302
328
|
return { program, getAdapterForExchange, isJson };
|
|
303
329
|
}
|
|
330
|
+
/** `perp trade <leaf>` commands that only read (order status, fills, P&L tracking, spot reads). */
|
|
331
|
+
const TRADE_READ_COMMANDS = new Set(["status", "fills", "pnl-track", "spot-orders", "spot-balance"]);
|
|
332
|
+
/**
|
|
333
|
+
* The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
|
|
334
|
+
* `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
|
|
335
|
+
* (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
|
|
336
|
+
* before the dry-run guard and before any action, so a dry-run plan can never present a gated
|
|
337
|
+
* venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
|
|
338
|
+
*/
|
|
339
|
+
export function assertCommandVenueRole(actionCommand, exchangeOpt) {
|
|
340
|
+
const path = commandPath(actionCommand).split(" ");
|
|
341
|
+
const leaf = path[path.length - 1];
|
|
342
|
+
let venueArgs;
|
|
343
|
+
let feature;
|
|
344
|
+
if (leaf === "multi" && (path[0] === "trade" || path.length === 1)) {
|
|
345
|
+
venueArgs = actionCommand.args.map(leg => leg.split(":")[0]);
|
|
346
|
+
feature = "trading";
|
|
347
|
+
}
|
|
348
|
+
else if (path[0] === "strategy" && typeof actionCommand.opts().exchanges === "string") {
|
|
349
|
+
// `--exchanges` legs (strategy run / funding-arb / quick-arb / delta-neutral, incl. their defaults):
|
|
350
|
+
// refused before the first leg's adapter is built. The -e venue is gated by the strategy adapter factory.
|
|
351
|
+
venueArgs = String(actionCommand.opts().exchanges).split(",").map(v => v.trim()).filter(v => v !== "");
|
|
352
|
+
feature = "automation";
|
|
353
|
+
}
|
|
354
|
+
else if (path[0] === "trade" && path.length === 2 && !TRADE_READ_COMMANDS.has(leaf)) {
|
|
355
|
+
venueArgs = [exchangeOpt];
|
|
356
|
+
feature = "trading";
|
|
357
|
+
}
|
|
358
|
+
else if (path[0] === "wallet" && path[1] === "agent" && ["approve", "import", "revoke", "rotate"].includes(path[2] ?? "")) {
|
|
359
|
+
// `revoke --force` / `--local-only` only clears local records (nothing signed or sent): allowed.
|
|
360
|
+
const o = actionCommand.opts();
|
|
361
|
+
if (path[2] === "revoke" && (o.force === true || o.localOnly === true))
|
|
362
|
+
return;
|
|
363
|
+
venueArgs = actionCommand.args.slice(0, 1);
|
|
364
|
+
feature = path[2] === "import" ? "agentImport" : `agent ${path[2]}`;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const v of venueArgs) {
|
|
370
|
+
const id = tryResolveVenueId(v);
|
|
371
|
+
if (id !== null && !getVenue(id).roles.includes("trading"))
|
|
372
|
+
throw roleUnsupported(getVenue(id), feature);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** `perp a b c` path of a command (root name excluded). */
|
|
376
|
+
function commandPath(cmd) {
|
|
377
|
+
const names = [];
|
|
378
|
+
for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
|
|
379
|
+
names.unshift(c.name());
|
|
380
|
+
return names.join(" ");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* `perp help <group> <leaf> ...` shows the help of the deepest named command. Commander's built-in
|
|
384
|
+
* root help command resolves only its first operand, so `perp help market list` printed the
|
|
385
|
+
* `market` group help. Groups keep their own built-in `help` subcommand (`perp market help list`).
|
|
386
|
+
*/
|
|
387
|
+
function installNestedHelp(program) {
|
|
388
|
+
program.helpCommand(false);
|
|
389
|
+
program
|
|
390
|
+
.command("help [command...]")
|
|
391
|
+
.description("display help for command (any depth: perp help trade limit)")
|
|
392
|
+
.action((names) => {
|
|
393
|
+
let cmd = program;
|
|
394
|
+
for (const name of names) {
|
|
395
|
+
const sub = cmd.commands.find(c => c.name() === name || c.aliases().includes(name));
|
|
396
|
+
if (sub === undefined) {
|
|
397
|
+
const where = cmd === program ? "perp" : `perp ${commandPath(cmd)}`;
|
|
398
|
+
throw new PerpError("INVALID_PARAMS", `Unknown command '${name}' under '${where}'`, {
|
|
399
|
+
remediation: `Available: ${cmd.commands.map(c => c.name()).join(", ")} — see: ${where} --help`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
cmd = sub;
|
|
403
|
+
}
|
|
404
|
+
cmd.help();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* A command group run without a subcommand (`perp history`): Commander prints the group's help as
|
|
409
|
+
* an error. In --json mode that reached stdout as a CLI_ERROR whose message was the usage text;
|
|
410
|
+
* it is now one INVALID_PARAMS document naming the subcommands, with remediation (exit 1).
|
|
411
|
+
* Human mode keeps the usage listing on stderr.
|
|
412
|
+
*/
|
|
413
|
+
function installMissingSubcommandError(program) {
|
|
414
|
+
program.on("beforeAllHelp", (ctx) => {
|
|
415
|
+
if (!ctx.error || program.opts().json !== true)
|
|
416
|
+
return;
|
|
417
|
+
const path = `perp ${commandPath(ctx.command)}`.trim();
|
|
418
|
+
const subs = ctx.command.commands.map(c => c.name()).filter(n => n !== "help");
|
|
419
|
+
throw new PerpError("INVALID_PARAMS", `'${path}' needs a subcommand: ${subs.join(", ")}`, {
|
|
420
|
+
remediation: `Run one of them, e.g. ${path} ${subs[0]} — list them with: ${path} --help`,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
@@ -90,6 +90,13 @@ export declare function importSecretLineCount(venue: VenueId): number;
|
|
|
90
90
|
export declare function importSecretFromLines(venue: VenueId, lines: string[]): ImportSecret;
|
|
91
91
|
/** Read the secret: hidden TTY prompt, else piped stdin (ondo: 2 lines key id + secret; others: 1 line). */
|
|
92
92
|
export declare function readImportSecret(venue: VenueId): Promise<ImportSecret>;
|
|
93
|
+
/**
|
|
94
|
+
* Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
|
|
95
|
+
* RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
|
|
96
|
+
* the trading permission; a signer created in the RISEx app keeps every permission (including
|
|
97
|
+
* deposit / withdraw), so it is never stored here.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assertImportSupported(venue: VenueId, network: Network): void;
|
|
93
100
|
/** Venues whose import cannot be checked against the venue (stored as "unverified"). */
|
|
94
101
|
export declare const UNVERIFIED_IMPORT_VENUES: readonly VenueId[];
|
|
95
102
|
/**
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import chalk from "chalk";
|
|
25
25
|
import { PerpError, classifyError } from "../errors.js";
|
|
26
26
|
import { acquireLock, releaseLock, getAgent, setAgent, deleteAgent, listAgents, agentNetworkOf, agentSourceOf, getVenueAgent, setVenueAgent, deleteVenueAgent, } from "../agent-wallet/store.js";
|
|
27
|
-
import { getVenue, networkConfig } from "../exchanges/venue.js";
|
|
27
|
+
import { getVenue, networkConfig, roleUnsupported } from "../exchanges/venue.js";
|
|
28
|
+
import { unsupported } from "../exchanges/trading-v2.js";
|
|
28
29
|
import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
|
|
29
30
|
import { loadOws } from "../signer/ows-loader.js";
|
|
30
31
|
import { defaultAgentName, isLighterFamilyVenue, parseExpiresIn, promptHidden, qfexWithdrawKeyWarning } from "./agent.js";
|
|
@@ -67,6 +68,11 @@ export function importSecretLabel(venue) {
|
|
|
67
68
|
case "arcus": return "Arcus API key private key (Ed25519)";
|
|
68
69
|
case "ondo": return "Ondo API secret";
|
|
69
70
|
case "qfex": return "QFEX API secret";
|
|
71
|
+
case "risex": return "RISEx signer private key";
|
|
72
|
+
case "txflow": return "TxFlow key (not supported)";
|
|
73
|
+
case "variational": return "Variational key (not supported)";
|
|
74
|
+
case "grvt": return "GRVT key (not supported)";
|
|
75
|
+
case "extended": return "Extended key (not supported)";
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
/**
|
|
@@ -154,6 +160,20 @@ function redacted(err, secrets, venue) {
|
|
|
154
160
|
details.remediation = scrub(e.remediation, forms);
|
|
155
161
|
return new PerpError(e.code, scrub(e.message, forms), details);
|
|
156
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
|
|
165
|
+
* RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
|
|
166
|
+
* the trading permission; a signer created in the RISEx app keeps every permission (including
|
|
167
|
+
* deposit / withdraw), so it is never stored here.
|
|
168
|
+
*/
|
|
169
|
+
export function assertImportSupported(venue, network) {
|
|
170
|
+
const def = getVenue(venue);
|
|
171
|
+
if (!def.roles.includes("trading"))
|
|
172
|
+
throw roleUnsupported(def, "agentImport");
|
|
173
|
+
if (venue === "risex") {
|
|
174
|
+
throw unsupported("risex", "agentImport", "perp-cli only uses RISEx signers it registered and restricted on-chain to trading-only permissions; an app-created signer keeps deposit / withdraw permissions", `Register a restricted signer with your master wallet: perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
157
177
|
/** Venues whose import cannot be checked against the venue (stored as "unverified"). */
|
|
158
178
|
export const UNVERIFIED_IMPORT_VENUES = ["pacifica"];
|
|
159
179
|
/**
|
|
@@ -722,6 +742,8 @@ async function importQfex(p, agentName, s, replacing) {
|
|
|
722
742
|
* appears in a thrown error.
|
|
723
743
|
*/
|
|
724
744
|
export async function importVenueAgent(p) {
|
|
745
|
+
// Refused before the secret is even inspected (nothing read, stored or sent).
|
|
746
|
+
assertImportSupported(p.venue, p.network);
|
|
725
747
|
const secrets = secretsOf(p.secret);
|
|
726
748
|
if (secrets.some(x => x.trim().length === 0))
|
|
727
749
|
throw new PerpError("INVALID_PARAMS", "Empty key — nothing was imported");
|
|
@@ -751,6 +773,13 @@ export async function importVenueAgent(p) {
|
|
|
751
773
|
case "lighter":
|
|
752
774
|
case "lighter-rh":
|
|
753
775
|
return await importLighter(p, p.venue, agentName, key, replacing);
|
|
776
|
+
case "risex":
|
|
777
|
+
case "txflow":
|
|
778
|
+
case "variational":
|
|
779
|
+
case "grvt":
|
|
780
|
+
case "extended":
|
|
781
|
+
// Unreachable: assertImportSupported() refused these before the lock.
|
|
782
|
+
throw new PerpError("CAPABILITY_UNSUPPORTED", `${p.venue} keys cannot be imported`);
|
|
754
783
|
}
|
|
755
784
|
}
|
|
756
785
|
catch (err) {
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta } from "../settings.js";
|
|
2
|
+
import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta, type RisexAgentMeta } from "../settings.js";
|
|
3
3
|
import "../exchanges/venues/index.js";
|
|
4
4
|
import type { Network, VenueId } from "../exchanges/venue-types.js";
|
|
5
5
|
import { type LighterVenueId } from "../exchanges/venues/lighter-family.js";
|
|
@@ -27,7 +27,7 @@ export interface VerifyResult {
|
|
|
27
27
|
/** Extract the agent list from a Pacifica /agent/list envelope; throws on an unrecognized shape. */
|
|
28
28
|
export declare function parsePacAgentList(json: unknown): unknown[];
|
|
29
29
|
/**
|
|
30
|
-
* Confirmation of the account a QFEX key was resolved to
|
|
30
|
+
* Confirmation of the account a QFEX key was resolved to. In a terminal: a
|
|
31
31
|
* y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
|
|
32
32
|
* that equals the resolved account counts — the resolved UUID is printed on stderr either way.
|
|
33
33
|
*/
|
|
@@ -94,6 +94,9 @@ export type ApproveAgentOutcome = {
|
|
|
94
94
|
} | {
|
|
95
95
|
venue: "arcus";
|
|
96
96
|
meta: ArcusAgentMeta;
|
|
97
|
+
} | {
|
|
98
|
+
venue: "risex";
|
|
99
|
+
meta: RisexAgentMeta;
|
|
97
100
|
} | {
|
|
98
101
|
venue: LighterVenueId;
|
|
99
102
|
agentName: string;
|
|
@@ -131,7 +134,7 @@ export declare function qfexWithdrawKeyWarning(meta: Pick<QfexAgentMeta, "agentN
|
|
|
131
134
|
/** Venues `wallet agent rotate` (and `approve --rotate`) can rotate. */
|
|
132
135
|
type RotatableVenue = "aster" | "hyperliquid" | "pacifica" | LighterVenueId;
|
|
133
136
|
type RotatedAgentOutcome = Exclude<ApproveAgentOutcome, {
|
|
134
|
-
venue: "ondo" | "arcus";
|
|
137
|
+
venue: "ondo" | "arcus" | "risex";
|
|
135
138
|
}>;
|
|
136
139
|
export interface RotateAgentParams {
|
|
137
140
|
exchange: RotatableVenue;
|