perp-cli 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
package/dist/commands/market.js
CHANGED
|
@@ -1,15 +1,139 @@
|
|
|
1
|
-
import { tapeSkippedOf } from "../exchanges/interface.js";
|
|
1
|
+
import { sortByTime, tapeSkippedOf } from "../exchanges/interface.js";
|
|
2
2
|
import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors, normalizeSymbolArg } from "../utils.js";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { PerpError } from "../errors.js";
|
|
5
5
|
import { midDecimal } from "../exchanges/precision.js";
|
|
6
|
-
import {
|
|
7
|
-
import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote } from "../exchanges/venue.js";
|
|
6
|
+
import { isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
|
|
7
|
+
import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote, withFundingVerified } from "../exchanges/venue.js";
|
|
8
8
|
import "../exchanges/venues/index.js";
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Venues of the multi-venue `market list|prices` (no -e): every venue with the marketData role
|
|
11
|
+
* and a (public) adapter — market-data-only venues (TxFlow, Variational, GRVT, Extended) included.
|
|
12
|
+
* They were left out in 0.15.0 because the list was the account-role venues.
|
|
13
|
+
*/
|
|
14
|
+
export const MARKET_DATA_VENUES = listVenuesWithRole("marketData").filter(v => v.create !== null).map(v => v.id);
|
|
15
|
+
/**
|
|
16
|
+
* `--limit` / `-l` / `--depth`: a positive integer, the same rule on every venue (parseInt took
|
|
17
|
+
* "abc" as NaN and "-5" as a negative slice). Anything else → INVALID_PARAMS before any request.
|
|
18
|
+
*/
|
|
19
|
+
export function parsePositiveIntArg(raw, flag) {
|
|
20
|
+
const t = raw.trim();
|
|
21
|
+
if (!/^\d+$/.test(t) || Number(t) < 1 || !Number.isSafeInteger(Number(t))) {
|
|
22
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be a positive integer (got "${raw}")`, { remediation: `Pass e.g. ${flag} 10` });
|
|
23
|
+
}
|
|
24
|
+
return Number(t);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Most order-book levels per side each venue serves (measured live 2026-10-19; the request size
|
|
28
|
+
* where the venue takes one): one rule for every venue — `--depth` above it is INVALID_PARAMS
|
|
29
|
+
* naming the max, never clamped or silently short. Absent = the full book (Extended).
|
|
30
|
+
*/
|
|
31
|
+
export const BOOK_DEPTH_MAX = {
|
|
32
|
+
hyperliquid: 20, pacifica: 10, lighter: 50, "lighter-rh": 50, aster: 50, arcus: 50, ondo: 50,
|
|
33
|
+
qfex: 20, txflow: 20, risex: 250, grvt: 500,
|
|
34
|
+
};
|
|
35
|
+
export function assertBookDepth(venue, depth) {
|
|
36
|
+
const id = tryResolveVenueId(venue);
|
|
37
|
+
const max = id === null ? undefined : BOOK_DEPTH_MAX[id];
|
|
38
|
+
if (max !== undefined && depth > max) {
|
|
39
|
+
throw new PerpError("INVALID_PARAMS", `--depth ${depth} exceeds ${venue}'s order book (max ${max} levels per side)`, {
|
|
40
|
+
exchange: venue, remediation: `Pass --depth 1..${max}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `--start` / `--end`: unix ms (digits) or an ISO-8601 date / time (`Date.parse`; "2026-10-18" is
|
|
46
|
+
* UTC midnight). parseInt("2026-10-18") read 2026 ms. Non-finite → INVALID_PARAMS.
|
|
47
|
+
*/
|
|
48
|
+
export function parseTimeArg(raw, flag) {
|
|
49
|
+
const t = raw.trim();
|
|
50
|
+
const ms = /^\d+$/.test(t) ? Number(t) : /^\d{4}-\d{2}-\d{2}/.test(t) ? Date.parse(t) : NaN;
|
|
51
|
+
// A unix-seconds value (10 digits today) read as ms lands in 1970: refused with a hint.
|
|
52
|
+
if (/^\d+$/.test(t) && ms < 1e11) {
|
|
53
|
+
throw new PerpError("INVALID_PARAMS", `${flag} ${raw} looks like unix seconds; pass milliseconds or an ISO date`, {
|
|
54
|
+
remediation: `Pass e.g. ${flag} ${t}000 or ${flag} 2026-10-18T00:00:00Z`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(ms)) {
|
|
58
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be unix milliseconds or an ISO date (got "${raw}")`, {
|
|
59
|
+
remediation: `Pass e.g. ${flag} 1792368000000 or ${flag} 2026-10-18T00:00:00Z`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return ms;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bare market symbol of a CLI argument as `market info` matches it against getMarkets() rows:
|
|
66
|
+
* "btc-perp" → "BTC", Extended "BTC-USD" / "TSLA_24_5-USD" → "BTC" / "TSLA", TxFlow "BTC-USDC" → "BTC"
|
|
67
|
+
* (book / funding resolve these through the venue; info read the raw argument and missed them).
|
|
68
|
+
* HIP-3 "xyz:XYZ100" keeps its prefix.
|
|
69
|
+
*/
|
|
70
|
+
export function bareSymbolOf(arg) {
|
|
71
|
+
return normalizeSymbolArg(arg.trim()).replace(/-PERP$/, "").replace(/-(USD|USDC|USDT)$/, "").replace(/_24_5$/, "");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* History order, one rule for every venue: trades and funding history newest first, candles
|
|
75
|
+
* oldest first (Aster / QFEX answered funding oldest first, GRVT trades oldest first). A stable
|
|
76
|
+
* copy — the adapter's array (and its non-enumerable tape metadata) is left as is.
|
|
77
|
+
*/
|
|
78
|
+
export function newestFirst(rows) {
|
|
79
|
+
return sortByTime(rows, "desc");
|
|
80
|
+
}
|
|
81
|
+
/** Human reason of a listed-but-untradable market, keyed on `status` (null when none is known). */
|
|
82
|
+
const NOT_TRADABLE_REASON = {
|
|
83
|
+
nonUsdQuote: "non-USD quote — readable, not tradable through perp-cli",
|
|
84
|
+
offHours: "outside the underlying's trading hours",
|
|
85
|
+
reduceOnly: "reduce-only",
|
|
86
|
+
halted: "trading halted",
|
|
87
|
+
};
|
|
88
|
+
export function notTradableReason(m) {
|
|
89
|
+
if (m.tradable !== false)
|
|
90
|
+
return null;
|
|
91
|
+
if (typeof m.status === "string" && m.status !== "")
|
|
92
|
+
return NOT_TRADABLE_REASON[m.status] ?? m.status;
|
|
93
|
+
return "not tradable right now";
|
|
94
|
+
}
|
|
95
|
+
/** Symbol cell; a listed market that is not tradable is marked with its reason. */
|
|
96
|
+
function symbolCell(m) {
|
|
97
|
+
const why = notTradableReason(m);
|
|
98
|
+
return why === null ? chalk.white.bold(m.symbol) : `${chalk.white.bold(m.symbol)} ${chalk.yellow(`(${why})`)}`;
|
|
99
|
+
}
|
|
100
|
+
/** Multi-venue human output: the footnote of every listed venue read through an unofficial API. */
|
|
101
|
+
function printUnofficialNotes(venues) {
|
|
102
|
+
for (const ex of venues) {
|
|
103
|
+
const id = tryResolveVenueId(ex);
|
|
104
|
+
const note = id === null ? null : unofficialApiNote(getVenue(id));
|
|
105
|
+
if (note !== null)
|
|
106
|
+
console.log(chalk.yellow(` * ${note}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* meta.warnings for market rows whose fundingRate is null for a known reason (Lighter: the
|
|
111
|
+
* settled-rate snapshot could not be read) — the reason reaches --json, not only stderr.
|
|
112
|
+
*/
|
|
113
|
+
function fundingWarnings(adapters, rows = null) {
|
|
114
|
+
// Tied to the rows served: no warning next to rows that all carry a rate (a cached listing).
|
|
115
|
+
if (rows !== null && !rows.some(r => r.fundingRate === null))
|
|
116
|
+
return undefined;
|
|
117
|
+
const w = adapters.map(a => a.fundingWarning?.() ?? null).filter((x) => x !== null);
|
|
118
|
+
return w.length > 0 ? { warnings: w } : undefined;
|
|
119
|
+
}
|
|
11
120
|
/** null = venue does not publish the value → "-" (never rendered as $0). */
|
|
12
121
|
const usdOrDash = (v) => (v === null ? chalk.gray("-") : `$${formatUsd(v)}`);
|
|
122
|
+
/** Non-USD quote currency of a row (QFEX "SAMSUNG-KRW": quoteAsset "KRW"); null = USD or not stated. */
|
|
123
|
+
const nonUsdQuoteOf = (m) => {
|
|
124
|
+
const q = m.quoteAsset;
|
|
125
|
+
return typeof q === "string" && q !== "USD" ? q : null;
|
|
126
|
+
};
|
|
127
|
+
/** A price of row `m` in its quote currency: "$1.23" for USD, "213,500 KRW" otherwise; null → "-". */
|
|
128
|
+
const priceOrDash = (v, m) => {
|
|
129
|
+
const q = nonUsdQuoteOf(m);
|
|
130
|
+
return v === null ? chalk.gray("-") : q === null ? `$${formatUsd(v)}` : `${formatUsd(v)} ${q}`;
|
|
131
|
+
};
|
|
132
|
+
/** 24h volume: USD, or (no USD figure) the venue's quote-currency notional labelled with its currency. */
|
|
133
|
+
const volumeCell = (m) => {
|
|
134
|
+
const local = m.volume24hQuote;
|
|
135
|
+
return m.volume24h === null && typeof local === "string" ? priceOrDash(local, m) : usdOrDash(m.volume24h);
|
|
136
|
+
};
|
|
13
137
|
/**
|
|
14
138
|
* Funding cell. On a venue whose funding interval is per market (VenueDefinition.fundingRateBasis
|
|
15
139
|
* "perMarket": Variational, Aster, GRVT) the raw rate is labelled with its interval ("+0.0079%/8h"), since
|
|
@@ -41,7 +165,7 @@ function formatOpenInterest(m, withUsd) {
|
|
|
41
165
|
const mark = Number(m.markPrice);
|
|
42
166
|
if (!withUsd || !Number.isFinite(oi) || !(mark > 0))
|
|
43
167
|
return base;
|
|
44
|
-
return `${base} ${chalk.gray(`(≈
|
|
168
|
+
return `${base} ${chalk.gray(`(≈ ${priceOrDash(String(oi * mark), m)} at mark)`)}`;
|
|
45
169
|
}
|
|
46
170
|
function printRfqQuotes(q, json) {
|
|
47
171
|
if (json)
|
|
@@ -110,11 +234,11 @@ getAccountAdapter) {
|
|
|
110
234
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
111
235
|
const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI (base)", "Max Lev"];
|
|
112
236
|
const marketRow = (m, perMarket = false) => [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
237
|
+
symbolCell(m),
|
|
238
|
+
priceOrDash(m.markPrice, m),
|
|
239
|
+
priceOrDash(m.indexPrice, m),
|
|
116
240
|
fundingCell(m, perMarket),
|
|
117
|
-
|
|
241
|
+
volumeCell(m),
|
|
118
242
|
formatOpenInterest(m, false),
|
|
119
243
|
m.maxLeverage === null ? chalk.gray("-") : String(m.maxLeverage) + "x",
|
|
120
244
|
];
|
|
@@ -122,17 +246,17 @@ getAccountAdapter) {
|
|
|
122
246
|
if (!explicitExchange && getAdapterForExchange) {
|
|
123
247
|
const grouped = {};
|
|
124
248
|
const errors = {};
|
|
125
|
-
|
|
249
|
+
const listed = [];
|
|
250
|
+
await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
|
|
126
251
|
try {
|
|
127
252
|
const adapter = await getAdapterForExchange(ex);
|
|
128
253
|
grouped[ex] = await adapter.getMarkets();
|
|
254
|
+
listed.push(adapter);
|
|
129
255
|
// --hip3: fetch HIP-3 dex markets
|
|
130
256
|
if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
|
|
131
257
|
const dexes = await adapter.listDeployedDexes();
|
|
132
258
|
await Promise.allSettled(dexes.map(async (d) => {
|
|
133
|
-
const
|
|
134
|
-
dexAdapter.setDex(d.name);
|
|
135
|
-
const markets = await dexAdapter.getMarkets();
|
|
259
|
+
const markets = await adapter.forDex(d.name).getMarkets();
|
|
136
260
|
if (markets.length > 0)
|
|
137
261
|
grouped[`hip3:${d.name}`] = markets;
|
|
138
262
|
}));
|
|
@@ -143,7 +267,7 @@ getAccountAdapter) {
|
|
|
143
267
|
}
|
|
144
268
|
}));
|
|
145
269
|
if (isJson()) {
|
|
146
|
-
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
270
|
+
return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
|
|
147
271
|
}
|
|
148
272
|
// Per-exchange tables
|
|
149
273
|
for (const [ex, markets] of Object.entries(grouped)) {
|
|
@@ -157,6 +281,7 @@ getAccountAdapter) {
|
|
|
157
281
|
for (const [ex, msg] of Object.entries(errors)) {
|
|
158
282
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
159
283
|
}
|
|
284
|
+
printUnofficialNotes(Object.keys(grouped));
|
|
160
285
|
return;
|
|
161
286
|
}
|
|
162
287
|
// Single exchange mode
|
|
@@ -167,18 +292,16 @@ getAccountAdapter) {
|
|
|
167
292
|
if (opts.hip3 && isDexCapable(adapter)) {
|
|
168
293
|
const dexes = await adapter.listDeployedDexes();
|
|
169
294
|
await Promise.allSettled(dexes.map(async (d) => {
|
|
170
|
-
const
|
|
171
|
-
dexAdapter.setDex(d.name);
|
|
172
|
-
const m = await dexAdapter.getMarkets();
|
|
295
|
+
const m = await adapter.forDex(d.name).getMarkets();
|
|
173
296
|
if (m.length > 0)
|
|
174
297
|
hip3[d.name] = m;
|
|
175
298
|
}));
|
|
176
299
|
}
|
|
177
300
|
if (isJson()) {
|
|
178
301
|
if (Object.keys(hip3).length > 0) {
|
|
179
|
-
return printJson(jsonOk({ main: markets, hip3 }));
|
|
302
|
+
return printJson(jsonOk({ main: withFundingVerified(adapter.name, markets), hip3: Object.fromEntries(Object.entries(hip3).map(([d, rows]) => [d, withFundingVerified(adapter.name, rows)])) }));
|
|
180
303
|
}
|
|
181
|
-
return printJson(jsonOk(markets));
|
|
304
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
|
|
182
305
|
}
|
|
183
306
|
const perMarket = perMarketFunding(adapter.name);
|
|
184
307
|
// Rows that carry an asset class (Extended: crypto / equity / etf / index / commodity / fx / preIpo) show it.
|
|
@@ -204,25 +327,27 @@ getAccountAdapter) {
|
|
|
204
327
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
205
328
|
const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
|
|
206
329
|
const priceRow = (m, perMarket = false) => [
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
330
|
+
symbolCell(m),
|
|
331
|
+
priceOrDash(m.markPrice, m),
|
|
332
|
+
priceOrDash(m.indexPrice, m),
|
|
210
333
|
fundingCell(m, perMarket),
|
|
211
334
|
];
|
|
212
335
|
if (!explicitExchange && getAdapterForExchange) {
|
|
213
336
|
const grouped = {};
|
|
214
337
|
const errors = {};
|
|
215
|
-
|
|
338
|
+
const listed = [];
|
|
339
|
+
await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
|
|
216
340
|
try {
|
|
217
341
|
const adapter = await getAdapterForExchange(ex);
|
|
218
342
|
grouped[ex] = await adapter.getMarkets();
|
|
343
|
+
listed.push(adapter);
|
|
219
344
|
}
|
|
220
345
|
catch (err) {
|
|
221
346
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
222
347
|
}
|
|
223
348
|
}));
|
|
224
349
|
if (isJson()) {
|
|
225
|
-
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
350
|
+
return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
|
|
226
351
|
}
|
|
227
352
|
for (const [ex, markets] of Object.entries(grouped)) {
|
|
228
353
|
if (markets.length === 0)
|
|
@@ -232,29 +357,15 @@ getAccountAdapter) {
|
|
|
232
357
|
}
|
|
233
358
|
for (const [ex, msg] of Object.entries(errors))
|
|
234
359
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
360
|
+
printUnofficialNotes(Object.keys(grouped));
|
|
235
361
|
return;
|
|
236
362
|
}
|
|
237
363
|
const adapter = await getAdapter();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const rows = prices.map((p) => [
|
|
244
|
-
chalk.white.bold(p.symbol),
|
|
245
|
-
`$${formatUsd(p.mark)}`,
|
|
246
|
-
`$${formatUsd(p.mid)}`,
|
|
247
|
-
`$${formatUsd(p.oracle)}`,
|
|
248
|
-
formatPercent(p.funding),
|
|
249
|
-
]);
|
|
250
|
-
console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
const markets = await adapter.getMarkets();
|
|
254
|
-
if (isJson())
|
|
255
|
-
return printJson(jsonOk(markets));
|
|
256
|
-
console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m, perMarketFunding(adapter.name)))));
|
|
257
|
-
}
|
|
364
|
+
// One normalized shape on every venue (Pacifica included — it printed the raw /info/prices rows).
|
|
365
|
+
const markets = await adapter.getMarkets();
|
|
366
|
+
if (isJson())
|
|
367
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
|
|
368
|
+
console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m, perMarketFunding(adapter.name)))));
|
|
258
369
|
});
|
|
259
370
|
market
|
|
260
371
|
.command("info <symbol>")
|
|
@@ -268,20 +379,25 @@ getAccountAdapter) {
|
|
|
268
379
|
return void await withJsonErrors(isJson(), async () => {
|
|
269
380
|
const one = await single.call(adapter, symbol);
|
|
270
381
|
if (isJson())
|
|
271
|
-
return printJson(jsonOk(one));
|
|
382
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, [one])[0]));
|
|
272
383
|
printMarketInfo(adapter.name, one);
|
|
273
384
|
});
|
|
274
385
|
}
|
|
275
386
|
const markets = await adapter.getMarkets();
|
|
276
387
|
const sym = symbol.toUpperCase();
|
|
388
|
+
const bare = bareSymbolOf(symbol).toUpperCase();
|
|
277
389
|
const m = markets.find((mk) => {
|
|
278
390
|
const s = mk.symbol.toUpperCase();
|
|
279
|
-
|
|
280
|
-
|
|
391
|
+
const sBare = s.replace(/-PERP$/, "");
|
|
392
|
+
return s === sym || sBare === sym || sBare === bare
|
|
393
|
+
|| s.split(":").pop() === sym || s.split(":").pop() === bare; // HIP-3: "km:US500" matches "US500"
|
|
281
394
|
});
|
|
282
395
|
if (!m) {
|
|
283
396
|
// Printed (envelope / Error + Remediation) and exit 1 via withJsonErrors.
|
|
284
397
|
return void await withJsonErrors(isJson(), async () => {
|
|
398
|
+
const delistedErr = adapter.delistedError?.(symbol) ?? null;
|
|
399
|
+
if (delistedErr !== null)
|
|
400
|
+
throw delistedErr;
|
|
285
401
|
const delisted = typeof adapter.delistedMessage === "function"
|
|
286
402
|
? adapter.delistedMessage(symbol)
|
|
287
403
|
: null;
|
|
@@ -292,7 +408,7 @@ getAccountAdapter) {
|
|
|
292
408
|
});
|
|
293
409
|
}
|
|
294
410
|
if (isJson())
|
|
295
|
-
return printJson(jsonOk(m));
|
|
411
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, [m])[0], fundingWarnings([adapter], [m])));
|
|
296
412
|
printMarketInfo(adapter.name, m);
|
|
297
413
|
});
|
|
298
414
|
market
|
|
@@ -300,15 +416,19 @@ getAccountAdapter) {
|
|
|
300
416
|
.description("Show orderbook for a symbol")
|
|
301
417
|
.option("-d, --depth <n>", "Number of levels", "10")
|
|
302
418
|
.action(async (symbol, opts) => {
|
|
419
|
+
const depth = parsePositiveIntArg(opts.depth, "--depth");
|
|
303
420
|
const adapter = await getAdapter();
|
|
304
421
|
const sym = normalizeSymbolArg(symbol);
|
|
305
422
|
// RFQ venue (Variational): no order book — the maker's indicative quotes, labelled as such.
|
|
306
423
|
if (adapter.getRfqQuotes !== undefined)
|
|
307
424
|
return printRfqQuotes(await adapter.getRfqQuotes(sym), isJson());
|
|
308
|
-
|
|
425
|
+
// `depth` reaches venues that size the request by it (GRVT, RISEx); every answer is cut to
|
|
426
|
+
// `depth` levels per side — JSON included (it printed the venue's full book).
|
|
427
|
+
assertBookDepth(adapter.name, depth);
|
|
428
|
+
const full = await adapter.getOrderbook(sym, depth);
|
|
429
|
+
const book = { ...full, bids: full.bids.slice(0, depth), asks: full.asks.slice(0, depth) };
|
|
309
430
|
if (isJson())
|
|
310
431
|
return printJson(jsonOk(book));
|
|
311
|
-
const depth = parseInt(opts.depth);
|
|
312
432
|
const asks = book.asks.slice(0, depth).reverse();
|
|
313
433
|
const bids = book.bids.slice(0, depth);
|
|
314
434
|
// Compute spread
|
|
@@ -329,21 +449,24 @@ getAccountAdapter) {
|
|
|
329
449
|
return "█".repeat(len);
|
|
330
450
|
};
|
|
331
451
|
console.log(chalk.cyan.bold(`\n Orderbook: ${sym} (${adapter.name})\n`));
|
|
332
|
-
|
|
452
|
+
// A book with a non-USD quoteAsset (QFEX SAMSUNG-KRW) shows its currency, never a $.
|
|
453
|
+
const quote = nonUsdQuoteOf(book);
|
|
454
|
+
const money = (v) => priceOrDash(String(v), book);
|
|
455
|
+
console.log(chalk.gray(` Price Size ${quote === null ? "Cum$ " : `Cum (${quote})`.padEnd(14)} Depth`));
|
|
333
456
|
console.log(chalk.gray(" " + "─".repeat(60)));
|
|
334
457
|
asks.forEach(([p, a], i) => {
|
|
335
458
|
const cumUsd = askCums[i] * (Number(p));
|
|
336
|
-
console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)}
|
|
459
|
+
console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
|
|
337
460
|
chalk.red.dim(bar(askCums[i])));
|
|
338
461
|
});
|
|
339
462
|
// Spread line
|
|
340
463
|
const spreadStr = spreadAbs > 0
|
|
341
|
-
?
|
|
464
|
+
? `${money(spreadAbs)} (${spreadPct.toFixed(3)}%)`
|
|
342
465
|
: "─";
|
|
343
466
|
console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
|
|
344
467
|
bids.forEach(([p, a], i) => {
|
|
345
468
|
const cumUsd = bidCums[i] * (Number(p));
|
|
346
|
-
console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)}
|
|
469
|
+
console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
|
|
347
470
|
chalk.green.dim(bar(bidCums[i])));
|
|
348
471
|
});
|
|
349
472
|
console.log();
|
|
@@ -351,11 +474,15 @@ getAccountAdapter) {
|
|
|
351
474
|
market
|
|
352
475
|
.command("trades <symbol>")
|
|
353
476
|
.description("Recent trades for a symbol")
|
|
354
|
-
.option("--limit <n>", "Number of recent trades to show
|
|
477
|
+
.option("--limit <n>", "Number of recent trades to show (default: the venue's own default, within its cap)")
|
|
355
478
|
.action(async (symbol, opts) => {
|
|
479
|
+
// No --limit → undefined: each adapter applies its own default within its venue cap, so a
|
|
480
|
+
// plain `market trades` never trips a cap (HL serves 10).
|
|
481
|
+
const limit = opts.limit === undefined ? undefined : parsePositiveIntArg(opts.limit, "--limit");
|
|
356
482
|
const adapter = await getAdapter();
|
|
357
|
-
const
|
|
358
|
-
const
|
|
483
|
+
const raw = await adapter.getRecentTrades(normalizeSymbolArg(symbol), limit);
|
|
484
|
+
const trades = newestFirst(raw);
|
|
485
|
+
const skipped = tapeSkippedOf(trades); // metadata survives the sort (sortByTime)
|
|
359
486
|
if (isJson())
|
|
360
487
|
return printJson(jsonOk(trades, skipped !== null ? { skippedRows: skipped } : undefined));
|
|
361
488
|
if (skipped !== null)
|
|
@@ -377,31 +504,35 @@ getAccountAdapter) {
|
|
|
377
504
|
.description("Funding rate history")
|
|
378
505
|
.option("-l, --limit <n>", "Number of records", "10")
|
|
379
506
|
.action(async (symbol, opts) => {
|
|
507
|
+
const limit = parsePositiveIntArg(opts.limit, "--limit");
|
|
380
508
|
const adapter = await getAdapter();
|
|
381
509
|
// A venue without a funding-history API (Variational): its current funding terms, labelled as such.
|
|
382
510
|
if (adapter.getFundingTerms !== undefined)
|
|
383
511
|
return printFundingTerms(await adapter.getFundingTerms(normalizeSymbolArg(symbol)), isJson());
|
|
384
|
-
const history = await adapter.getFundingHistory(normalizeSymbolArg(symbol),
|
|
512
|
+
const history = newestFirst(await adapter.getFundingHistory(normalizeSymbolArg(symbol), limit));
|
|
385
513
|
if (isJson())
|
|
386
514
|
return printJson(jsonOk(history));
|
|
387
515
|
if (history.length === 0) {
|
|
388
516
|
console.log(chalk.gray("\n No funding history.\n"));
|
|
389
517
|
return;
|
|
390
518
|
}
|
|
391
|
-
|
|
519
|
+
// `price` is the venue's reference price of the print — oracle (Pacifica), mark (GRVT, Aster)
|
|
520
|
+
// or index (RISEx) — so the column is "Price", not "Oracle"; each rate shows its interval.
|
|
521
|
+
const hasPrice = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
|
|
392
522
|
const rows = history.map((h) => {
|
|
393
523
|
const base = [
|
|
394
524
|
new Date(h.time).toLocaleString(),
|
|
395
|
-
formatPercent(h.rate),
|
|
525
|
+
h.rate === null ? chalk.gray(h.marketClosed === true ? "market closed" : "N/A") : formatPercent(h.rate),
|
|
526
|
+
h.intervalHours === null ? chalk.gray("-") : `${h.intervalHours}h`,
|
|
396
527
|
];
|
|
397
|
-
if (
|
|
528
|
+
if (hasPrice)
|
|
398
529
|
base.push(h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"));
|
|
399
530
|
return base;
|
|
400
531
|
});
|
|
401
|
-
const headers =
|
|
532
|
+
const headers = hasPrice ? ["Time", "Funding Rate", "Interval", "Price"] : ["Time", "Funding Rate", "Interval"];
|
|
402
533
|
console.log(makeTable(headers, rows));
|
|
403
|
-
if (!
|
|
404
|
-
console.log(chalk.gray(" Note:
|
|
534
|
+
if (!hasPrice)
|
|
535
|
+
console.log(chalk.gray(" Note: no reference price published with this venue's funding prints.\n"));
|
|
405
536
|
});
|
|
406
537
|
market
|
|
407
538
|
.command("mid <symbol>")
|
|
@@ -448,16 +579,22 @@ getAccountAdapter) {
|
|
|
448
579
|
market
|
|
449
580
|
.command("kline <symbol> <interval>")
|
|
450
581
|
.description("Kline/candlestick data (intervals: 1m,5m,15m,1h,4h,1d,...)")
|
|
451
|
-
.option("--start <ts>", "Start time (unix ms)")
|
|
452
|
-
.option("--end <ts>", "End time (unix ms)")
|
|
582
|
+
.option("--start <ts>", "Start time (unix ms or ISO date, e.g. 2026-10-18T00:00:00Z)")
|
|
583
|
+
.option("--end <ts>", "End time (unix ms or ISO date)")
|
|
453
584
|
.action(async (symbol, interval, opts) => {
|
|
454
585
|
const publicAdapter = await getAdapter();
|
|
455
586
|
// A venue whose candles endpoint is authenticated (Ondo) is read with the registered key.
|
|
456
587
|
const adapter = getAccountAdapter !== undefined && klinesNeedAuth(publicAdapter) ? await getAccountAdapter() : publicAdapter;
|
|
457
588
|
const now = Date.now();
|
|
458
|
-
const startTime = opts.start ?
|
|
459
|
-
const endTime = opts.end ?
|
|
460
|
-
|
|
589
|
+
const startTime = opts.start !== undefined ? parseTimeArg(opts.start, "--start") : now - 24 * 60 * 60 * 1000;
|
|
590
|
+
const endTime = opts.end !== undefined ? parseTimeArg(opts.end, "--end") : now;
|
|
591
|
+
if (startTime > endTime) {
|
|
592
|
+
throw new PerpError("INVALID_PARAMS", `--start (${new Date(startTime).toISOString()}) is after --end (${new Date(endTime).toISOString()})`, {
|
|
593
|
+
remediation: "Pass --start before --end",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// Candles oldest first (ascending open time) on every venue.
|
|
597
|
+
const klines = sortByTime(await adapter.getKlines(normalizeSymbolArg(symbol), interval, startTime, endTime), "asc");
|
|
461
598
|
if (isJson())
|
|
462
599
|
return printJson(jsonOk(klines));
|
|
463
600
|
if (klines.length === 0) {
|
|
@@ -467,10 +604,11 @@ getAccountAdapter) {
|
|
|
467
604
|
// ISO (UTC) candle open times — one format for every venue, independent of the host locale.
|
|
468
605
|
const rows = klines.slice(-20).map((k) => [
|
|
469
606
|
new Date(k.time).toISOString(),
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
607
|
+
// A row with a non-USD quoteAsset (QFEX SAMSUNG-KRW) prints its currency, never a $.
|
|
608
|
+
priceOrDash(k.open, k),
|
|
609
|
+
priceOrDash(k.high, k),
|
|
610
|
+
priceOrDash(k.low, k),
|
|
611
|
+
priceOrDash(k.close, k),
|
|
474
612
|
k.volume === null ? chalk.gray("-") : k.volume,
|
|
475
613
|
k.trades === null ? chalk.gray("-") : String(k.trades),
|
|
476
614
|
]);
|
|
@@ -501,7 +639,8 @@ getAccountAdapter) {
|
|
|
501
639
|
const rows = dexes.map(d => [
|
|
502
640
|
chalk.white.bold(d.name),
|
|
503
641
|
chalk.gray(d.deployer ? d.deployer.slice(0, 6) + "..." + d.deployer.slice(-4) : "-"),
|
|
504
|
-
|
|
642
|
+
// Live (non-delisted) assets; a dex whose every asset is delisted is marked as such.
|
|
643
|
+
d.delisted === true ? chalk.gray("delisted") : String(d.assets.length),
|
|
505
644
|
d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
|
|
506
645
|
]);
|
|
507
646
|
console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
|
|
@@ -510,13 +649,37 @@ getAccountAdapter) {
|
|
|
510
649
|
});
|
|
511
650
|
});
|
|
512
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Why a listed market is not tradable, from the row's `status` when the venue states it (QFEX
|
|
654
|
+
* "nonUsdQuote", Ondo "closed" / "reduceOnly"); the generic off-hours text only when it does not.
|
|
655
|
+
* null = tradable or no tradability flag.
|
|
656
|
+
*/
|
|
657
|
+
export function notTradableNote(m) {
|
|
658
|
+
const r = m;
|
|
659
|
+
if (r.tradable !== false)
|
|
660
|
+
return null;
|
|
661
|
+
if (r.status === "nonUsdQuote")
|
|
662
|
+
return `Read-only in perp-cli: quoted in ${typeof r.quoteAsset === "string" ? r.quoteAsset : "a non-USD currency"} (orders, sizing and risk limits are USD-only).`;
|
|
663
|
+
if (r.status === "closed")
|
|
664
|
+
return "Not tradable: the venue has closed this market to new trading.";
|
|
665
|
+
if (r.status === "reduceOnly")
|
|
666
|
+
return "Reduce-only on the venue: positions can be closed, not opened.";
|
|
667
|
+
return "Not tradable right now (off-hours, reduce-only or halted on the venue).";
|
|
668
|
+
}
|
|
513
669
|
function printMarketInfo(venue, m) {
|
|
514
670
|
console.log(chalk.cyan.bold(`\n ${m.symbol} on ${venue}\n`));
|
|
515
|
-
|
|
516
|
-
|
|
671
|
+
const note = notTradableNote(m);
|
|
672
|
+
if (note !== null)
|
|
673
|
+
console.log(chalk.yellow(` ${note}`));
|
|
674
|
+
console.log(` Mark Price: ${priceOrDash(m.markPrice, m)}`);
|
|
675
|
+
console.log(` Index Price: ${priceOrDash(m.indexPrice, m)}`);
|
|
517
676
|
// Per-market funding interval (GRVT 4h / 8h, Variational, Aster): the rate carries its interval.
|
|
518
677
|
console.log(` Funding Rate: ${fundingCell(m, perMarketFunding(venue))}`);
|
|
519
|
-
|
|
678
|
+
// A venue whose rate is unverified (QFEX) labels it on the row.
|
|
679
|
+
const fundingNote = m.fundingNote;
|
|
680
|
+
if (typeof fundingNote === "string")
|
|
681
|
+
console.log(chalk.yellow(` ${fundingNote}`));
|
|
682
|
+
console.log(` 24h Volume: ${volumeCell(m)}`);
|
|
520
683
|
console.log(` Open Interest: ${formatOpenInterest(m, true)}`);
|
|
521
684
|
console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
|
|
522
685
|
console.log();
|
|
@@ -7,3 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
export declare function registerOptionsCommands(program: Command, isJson: () => boolean): void;
|
|
10
|
+
/**
|
|
11
|
+
* Greeks by significant figures (3), not fixed decimals: toFixed(3) printed theta / vega of cheap
|
|
12
|
+
* options as -0.000. Derive publishes greeks at a 1e-5 resolution, so "0" can mean |x| < 0.00001.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fmtGreek(v: string | null | undefined): string;
|
package/dist/commands/options.js
CHANGED
|
@@ -90,8 +90,21 @@ function fmtPx(v) {
|
|
|
90
90
|
function fmtPct(v) {
|
|
91
91
|
return v === null ? dash : `${(Number(v) * 100).toFixed(1)}%`;
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Greeks by significant figures (3), not fixed decimals: toFixed(3) printed theta / vega of cheap
|
|
95
|
+
* options as -0.000. Derive publishes greeks at a 1e-5 resolution, so "0" can mean |x| < 0.00001.
|
|
96
|
+
*/
|
|
97
|
+
export function fmtGreek(v) {
|
|
98
|
+
if (v === null || v === undefined)
|
|
99
|
+
return dash;
|
|
100
|
+
const n = Number(v);
|
|
101
|
+
if (!Number.isFinite(n))
|
|
102
|
+
return dash;
|
|
103
|
+
if (n === 0)
|
|
104
|
+
return "0";
|
|
105
|
+
// Fixed notation, 3 significant figures (String() switches to 1.23e-7 below 1e-6).
|
|
106
|
+
const decimals = Math.max(0, 2 - Math.floor(Math.log10(Math.abs(n))));
|
|
107
|
+
return n.toFixed(Math.min(decimals, 20)).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
|
|
95
108
|
}
|
|
96
109
|
function sideCells(q) {
|
|
97
110
|
if (q === null)
|
|
@@ -130,5 +143,9 @@ function printQuote(q) {
|
|
|
130
143
|
["rho", fmtGreek(q.greeks?.rho), "OI", q.openInterest],
|
|
131
144
|
["24h contracts", q.stats24h.contracts, "24h trades", String(q.stats24h.trades)],
|
|
132
145
|
];
|
|
133
|
-
console.log(makeTable(["field", "value", "field", "value"], rows)
|
|
146
|
+
console.log(makeTable(["field", "value", "field", "value"], rows));
|
|
147
|
+
if (q.venue === "derive")
|
|
148
|
+
console.log(chalk.gray(" Greeks: 3 significant figures; Derive publishes them at a 1e-5 resolution (0 = |value| < 0.00001).\n"));
|
|
149
|
+
else
|
|
150
|
+
console.log();
|
|
134
151
|
}
|