@perpscope/percolator-adapter 0.4.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.
@@ -0,0 +1,1672 @@
1
+ import { normalizeFundingSkewHistory } from "./funding-history.js";
2
+
3
+ const KEYPAIR_FIELD_PATTERN = /(^|_)(secret|private|keypair|mnemonic|seed|walletPath|wallet)(_|$)/i;
4
+ const HISTORY_COMMAND_KEYS = new Set([
5
+ "fundinghistory",
6
+ "fundingskewhistory",
7
+ "fundingrates",
8
+ "skewhistory",
9
+ "marketcarryhistory"
10
+ ]);
11
+
12
+ export function assertReadOnlySnapshot(value, path = "snapshot") {
13
+ if (!value || typeof value !== "object") return;
14
+ for (const [key, child] of Object.entries(value)) {
15
+ const nextPath = `${path}.${key}`;
16
+ if (KEYPAIR_FIELD_PATTERN.test(key) || /secret|private|keypair|mnemonic|seed|walletpath|wallet/.test(normalizeKey(key))) {
17
+ throw new Error(`Refusing secret-bearing field in read-only snapshot: ${nextPath}`);
18
+ }
19
+ if (child && typeof child === "object") {
20
+ assertReadOnlySnapshot(child, nextPath);
21
+ }
22
+ }
23
+ }
24
+
25
+ export function detectPercolatorInputShape(input) {
26
+ if (!input || typeof input !== "object") return "unknown";
27
+ if (Array.isArray(input)) return isFundingHistoryArray(input) ? "funding-skew-history" : "percolator-market-array";
28
+ if (isReadOnlyRpcInput(input)) return "read-only-rpc-fetch";
29
+ if (isFundingHistoryInput(input)) return "funding-skew-history";
30
+ if (Array.isArray(input.commands) || input.command || hasCliSections(input)) return "percolator-cli-bundle";
31
+ if (Array.isArray(input.markets)) return "perpscope-snapshot";
32
+ return "unknown";
33
+ }
34
+
35
+ export function normalizePercolatorCliBundle(bundle) {
36
+ assertReadOnlySnapshot(bundle);
37
+ return coercePercolatorCliBundle(bundle);
38
+ }
39
+
40
+ export function parsePercolatorJson(text) {
41
+ const source = String(text).trim();
42
+ try {
43
+ return JSON.parse(source);
44
+ } catch {
45
+ const extracted = extractJsonPayload(source);
46
+ if (!extracted) throw new Error("No JSON payload found.");
47
+ return JSON.parse(extracted);
48
+ }
49
+ }
50
+
51
+ export function normalizePercolatorSnapshot(input) {
52
+ assertReadOnlySnapshot(input);
53
+ const snapshot = coercePercolatorSnapshot(input);
54
+ const markets = (snapshot.markets || []).map((market) =>
55
+ toTerminalMarketDto(market, snapshot.currentSlot)
56
+ );
57
+ const aggregateHealth = average(markets.map((market) => market.healthScore));
58
+ return {
59
+ source: snapshot.source,
60
+ cluster: snapshot.cluster || "unknown",
61
+ currentSlot: snapshot.currentSlot || 0,
62
+ aggregateHealth,
63
+ aggregateStatus: labelFromScore(aggregateHealth),
64
+ markets
65
+ };
66
+ }
67
+
68
+ export function buildPercolatorCompatibilityReport(input, normalizedSnapshot) {
69
+ assertReadOnlySnapshot(input);
70
+ assertReadOnlyCompatibilityCapture(input);
71
+ const shape = detectPercolatorInputShape(input);
72
+ const scope = compatibilityScope(input);
73
+ assertReadOnlyCompatibilityCapture(scope, "capture.normalized");
74
+ const snapshot = normalizedSnapshot || normalizePercolatorSnapshot(input);
75
+ const context = {
76
+ input,
77
+ scope,
78
+ shape,
79
+ snapshot,
80
+ market: compatibilityMarket(firstItem(snapshot.markets), input)
81
+ };
82
+ const recognizedSections = COMPATIBILITY_SECTION_SPECS
83
+ .filter((section) => section.present(context))
84
+ .map((section) => ({
85
+ id: section.id,
86
+ label: section.label,
87
+ tone: section.tone ? section.tone(context) : "good",
88
+ detail: section.detail(context)
89
+ }));
90
+ const missingFields = COMPATIBILITY_FIELD_SPECS
91
+ .filter((field) => !field.present(context))
92
+ .map((field) => ({
93
+ field: field.field,
94
+ label: field.label,
95
+ severity: field.severity,
96
+ detail: field.detail
97
+ }));
98
+ const ignoredFields = ignoredCompatibilityFields(input);
99
+ const dangerCount = missingFields.filter((field) => field.severity === "danger").length;
100
+ const warningCount = missingFields.length - dangerCount;
101
+ const recognizedDataCount = recognizedSections.filter((section) => section.id !== "safety").length;
102
+ const unknownStatus = shape === "unknown" && recognizedDataCount === 0;
103
+ const unknownPenalty = unknownStatus ? 22 : shape === "unknown" ? 10 : 0;
104
+ const score = clamp(100 - dangerCount * 18 - warningCount * 8 - ignoredFields.length * 3 - unknownPenalty, 0, 100);
105
+ const tone = unknownStatus || dangerCount ? "danger" : warningCount || ignoredFields.length ? "warning" : "good";
106
+ const status = unknownStatus ? "unknown" : dangerCount || warningCount || ignoredFields.length ? "partial" : "compatible";
107
+ const compatible = status === "compatible";
108
+ const source = snapshot.source || {};
109
+ const commandSet = Array.isArray(source.commandSet) ? source.commandSet : commandNames(input);
110
+
111
+ return {
112
+ shape,
113
+ status,
114
+ compatible,
115
+ tone,
116
+ score,
117
+ recognizedSections,
118
+ missingFields,
119
+ ignoredFields,
120
+ source: {
121
+ label: source.label || stringOf(scope, ["label", "sourceLabel"], "decoded capture"),
122
+ mode: source.mode || "read-only",
123
+ commandSet,
124
+ cluster: snapshot.cluster || "unknown",
125
+ currentSlot: snapshot.currentSlot || 0,
126
+ marketCount: snapshot.markets.length,
127
+ slab: context.market.slab || "",
128
+ program: context.market.program || ""
129
+ },
130
+ summary: {
131
+ recognizedCount: recognizedSections.length,
132
+ missingCount: missingFields.length,
133
+ ignoredCount: ignoredFields.length,
134
+ marketCount: snapshot.markets.length,
135
+ commandCount: commandSet.length
136
+ }
137
+ };
138
+ }
139
+
140
+ const COMPATIBILITY_SECTION_SPECS = [
141
+ {
142
+ id: "safety",
143
+ label: "read-only safety",
144
+ present: () => true,
145
+ detail: () => "no secret-like fields found"
146
+ },
147
+ {
148
+ id: "market",
149
+ label: "market identity",
150
+ present: ({ scope, snapshot }) =>
151
+ marketListOf(scope).length > 0 ||
152
+ nonEmptyObject(objectOf(scope, ["market", "marketInfo", "metadata", "instrument"], {})) ||
153
+ snapshot.markets.some((market) => knownText(market.slab) && !/^unknown/i.test(market.slab)),
154
+ detail: ({ snapshot }) => `${snapshot.markets.length} market${snapshot.markets.length === 1 ? "" : "s"} mapped`
155
+ },
156
+ {
157
+ id: "price",
158
+ label: "oracle price",
159
+ present: hasPriceSignal,
160
+ detail: ({ market }) => market.price?.mark ? `$${market.price.mark.toFixed(market.base === "WIF" ? 3 : 2)} mark` : "price signal mapped"
161
+ },
162
+ {
163
+ id: "engine",
164
+ label: "crank engine",
165
+ present: hasEngineSignal,
166
+ tone: hasCrankAgeSignal ? () => "good" : () => "warning",
167
+ detail: ({ market }) => `${Number(market.crank?.ageSlots || 0).toFixed(0)} slot age`
168
+ },
169
+ {
170
+ id: "funding",
171
+ label: "carry rate",
172
+ present: hasFundingSignal,
173
+ detail: ({ market }) => `${Number(market.funding?.bpsPerHour || 0).toFixed(1)} bps/hr`
174
+ },
175
+ {
176
+ id: "structure",
177
+ label: "market structure",
178
+ present: hasMarketStructureSignal,
179
+ detail: ({ market }) => `${Number(market.marketStructure?.oiSkewPct || 0).toFixed(1)}% OI skew`
180
+ },
181
+ {
182
+ id: "account",
183
+ label: "account runway",
184
+ present: hasAccountSignal,
185
+ detail: ({ market }) => `${Number(market.account?.liquidationDistancePct || 0).toFixed(1)}% runway`
186
+ },
187
+ {
188
+ id: "execution",
189
+ label: "book quality",
190
+ present: hasExecutionSignal,
191
+ detail: ({ market }) => `${Number(market.execution?.spreadBps || 0).toFixed(1)} bps spread`
192
+ },
193
+ {
194
+ id: "receipts",
195
+ label: "fill receipts",
196
+ present: hasReceiptSignal,
197
+ detail: ({ scope, market }) => `${receiptCount(scope, market)} receipt${receiptCount(scope, market) === 1 ? "" : "s"}`
198
+ },
199
+ {
200
+ id: "history",
201
+ label: "carry history",
202
+ present: hasHistorySignal,
203
+ detail: ({ scope, market }) => `${historyCount(scope, market)} rows`
204
+ },
205
+ {
206
+ id: "provenance",
207
+ label: "provenance",
208
+ present: hasProvenanceSignal,
209
+ detail: ({ snapshot }) => snapshot.source?.label || snapshot.cluster || "source mapped"
210
+ }
211
+ ];
212
+
213
+ const COMPATIBILITY_FIELD_SPECS = [
214
+ {
215
+ field: "market.slab",
216
+ label: "slab address",
217
+ severity: "danger",
218
+ detail: "Needed to anchor the terminal view to one Percolator market.",
219
+ present: ({ market }) => knownText(market.slab) && !/^unknown/i.test(market.slab)
220
+ },
221
+ {
222
+ field: "market.program",
223
+ label: "program id",
224
+ severity: "danger",
225
+ detail: "Needed before a live terminal trusts decoded account ownership.",
226
+ present: ({ market }) => knownText(market.program) && !/^unknown/i.test(market.program)
227
+ },
228
+ {
229
+ field: "price.mark",
230
+ label: "mark price",
231
+ severity: "danger",
232
+ detail: "Needed for runway, impact, and account notional math.",
233
+ present: hasPriceSignal
234
+ },
235
+ {
236
+ field: "price.publishAgeSec",
237
+ label: "oracle age",
238
+ severity: "warning",
239
+ detail: "Keeps stale price reads visible instead of hidden in raw output.",
240
+ present: hasOracleAgeSignal
241
+ },
242
+ {
243
+ field: "crank.ageSlots",
244
+ label: "crank age",
245
+ severity: "warning",
246
+ detail: "Shows whether risk and funding state are lagging the latest slot.",
247
+ present: hasCrankAgeSignal
248
+ },
249
+ {
250
+ field: "funding.bpsPerHour",
251
+ label: "funding rate",
252
+ severity: "warning",
253
+ detail: "Makes carry pressure readable for traders watching positions.",
254
+ present: hasFundingSignal
255
+ },
256
+ {
257
+ field: "marketStructure.openInterestUsd",
258
+ label: "open interest",
259
+ severity: "warning",
260
+ detail: "Needed for skew and stress pressure cards.",
261
+ present: hasMarketStructureSignal
262
+ },
263
+ {
264
+ field: "account.positionNotionalUsd",
265
+ label: "position notional",
266
+ severity: "warning",
267
+ detail: "Needed for account-level buffer and liquidation runway.",
268
+ present: hasPositionNotionalSignal
269
+ },
270
+ {
271
+ field: "execution.bestBid/bestAsk",
272
+ label: "best bid / ask",
273
+ severity: "warning",
274
+ detail: "Needed to separate real spread from adapter fallbacks.",
275
+ present: hasExecutionSignal
276
+ },
277
+ {
278
+ field: "execution.receipts",
279
+ label: "fill receipts",
280
+ severity: "warning",
281
+ detail: "Adds latency, markout, and priority-fee context.",
282
+ present: hasReceiptSignal
283
+ },
284
+ {
285
+ field: "history.fundingSkew",
286
+ label: "funding / skew history",
287
+ severity: "warning",
288
+ detail: "Adds trend context beyond a single decoded snapshot.",
289
+ present: hasHistorySignal
290
+ }
291
+ ];
292
+
293
+ const COMPATIBILITY_MUTATING_KEYS = new Set([
294
+ "instruction",
295
+ "instructions",
296
+ "order",
297
+ "orders",
298
+ "send",
299
+ "sendtransaction",
300
+ "sign",
301
+ "signer",
302
+ "signtransaction",
303
+ "transaction",
304
+ "transactions"
305
+ ]);
306
+
307
+ const COMPATIBILITY_TOP_LEVEL_KEYS = new Set([
308
+ "account",
309
+ "accountInfo",
310
+ "accounts",
311
+ "bitmap",
312
+ "cluster",
313
+ "command",
314
+ "commands",
315
+ "config",
316
+ "currentSlot",
317
+ "current_slot",
318
+ "data",
319
+ "engine",
320
+ "execution",
321
+ "executionReceipts",
322
+ "expectations",
323
+ "fillReceipts",
324
+ "fixture",
325
+ "fixtureKind",
326
+ "fundingHistory",
327
+ "fundingSkewHistory",
328
+ "header",
329
+ "history",
330
+ "items",
331
+ "json",
332
+ "label",
333
+ "market",
334
+ "marketConfig",
335
+ "marketInfo",
336
+ "markets",
337
+ "metadata",
338
+ "network",
339
+ "oracle",
340
+ "output",
341
+ "outputs",
342
+ "params",
343
+ "price",
344
+ "prices",
345
+ "program",
346
+ "programId",
347
+ "receiptTimeline",
348
+ "receipts",
349
+ "result",
350
+ "rows",
351
+ "rpcRead",
352
+ "slab",
353
+ "slabAddress",
354
+ "slabBitmap",
355
+ "slabConfig",
356
+ "slabEngine",
357
+ "slabHeader",
358
+ "slabParams",
359
+ "source",
360
+ "sourceLabel",
361
+ "slot",
362
+ "stdout",
363
+ "stdoutText"
364
+ ].map(normalizeKey));
365
+
366
+ const COMPATIBILITY_COMMAND_KEYS = new Set([
367
+ "bestprice",
368
+ "executionreceipt",
369
+ "executionreceipts",
370
+ "fillreceipts",
371
+ "fills",
372
+ "fundinghistory",
373
+ "fundingrates",
374
+ "fundingskewhistory",
375
+ "listmarkets",
376
+ "marketcarryhistory",
377
+ "receiptlog",
378
+ "receipts",
379
+ "skewhistory",
380
+ "slabaccount",
381
+ "slabaccounts",
382
+ "slabbitmap",
383
+ "slabengine",
384
+ "slabget",
385
+ "slabparams"
386
+ ]);
387
+
388
+ function ignoredCompatibilityFields(input) {
389
+ if (!input || typeof input !== "object" || Array.isArray(input)) return [];
390
+ const ignored = [];
391
+ for (const [key] of Object.entries(input)) {
392
+ if (!COMPATIBILITY_TOP_LEVEL_KEYS.has(normalizeKey(key))) {
393
+ ignored.push({
394
+ path: key,
395
+ label: key,
396
+ reason: "top-level field is not part of the adapter map yet"
397
+ });
398
+ }
399
+ }
400
+ for (const command of commandNames(input)) {
401
+ if (!COMPATIBILITY_COMMAND_KEYS.has(normalizeKey(command))) {
402
+ ignored.push({
403
+ path: `commands.${command}`,
404
+ label: command,
405
+ reason: "command is preserved as provenance but not mapped"
406
+ });
407
+ }
408
+ }
409
+ return ignored.slice(0, 8);
410
+ }
411
+
412
+ function assertReadOnlyCompatibilityCapture(value, path = "capture") {
413
+ if (!value || typeof value !== "object") return;
414
+ for (const [key, child] of Object.entries(value)) {
415
+ const normalized = normalizeKey(key);
416
+ if (COMPATIBILITY_MUTATING_KEYS.has(normalized)) {
417
+ throw new Error(`Refusing mutating field in compatibility capture: ${path}.${key}`);
418
+ }
419
+ if (child && typeof child === "object") assertReadOnlyCompatibilityCapture(child, `${path}.${key}`);
420
+ }
421
+ }
422
+
423
+ function compatibilityScope(input) {
424
+ if (!input || typeof input !== "object") return { markets: [] };
425
+ if (Array.isArray(input)) return { history: isFundingHistoryArray(input) ? input : [], markets: input };
426
+ const scope = collectCliSections(input);
427
+ if (!isReadOnlyRpcInput(input)) return scope;
428
+ const account = objectOf(input, ["account", "accountInfo"], {});
429
+ const decoded = objectOf(account, ["decoded"], {});
430
+ const market = {
431
+ ...objectOf(input, ["market"], {}),
432
+ slab: stringOf(input, ["slab", "slabAddress", "pubkey"], ""),
433
+ program: stringOf(input, ["programId", "program", "owner"], "")
434
+ };
435
+ return {
436
+ ...scope,
437
+ ...decoded,
438
+ market,
439
+ account: objectOf(decoded, ["accountUsd", "account", "position"], scope.account || {}),
440
+ accounts: decoded.accounts || scope.accounts,
441
+ bestPrice: decoded.bestPrice || scope.bestPrice,
442
+ bitmap: decoded.bitmap || scope.bitmap,
443
+ config: decoded.config || scope.config,
444
+ engine: decoded.engine || scope.engine,
445
+ header: decoded.header || scope.header,
446
+ params: decoded.params || scope.params
447
+ };
448
+ }
449
+
450
+ function compatibilityMarket(market, input) {
451
+ if (!input || typeof input !== "object" || Array.isArray(input)) return market || {};
452
+ const inputMarket = objectOf(input, ["market", "marketInfo", "metadata", "instrument"], {});
453
+ return {
454
+ ...(market || {}),
455
+ slab: knownText(market?.slab) && !/^unknown/i.test(market.slab)
456
+ ? market.slab
457
+ : stringOf(inputMarket, ["slab", "slabAddress", "pubkey"], stringOf(input, ["slab", "slabAddress", "pubkey"], market?.slab || "")),
458
+ program: knownText(market?.program) && !/^unknown/i.test(market.program)
459
+ ? market.program
460
+ : stringOf(inputMarket, ["programId", "program", "owner"], stringOf(input, ["programId", "program", "owner"], market?.program || ""))
461
+ };
462
+ }
463
+
464
+ function hasPriceSignal({ scope, market }) {
465
+ const oracle = objectOf(scope, ["oracle", "price", "prices", "oraclePrice"], {});
466
+ const book = objectOf(scope, ["bestPrice", "best-price", "best_price", "book", "orderbook", "quote"], {});
467
+ const firstReceipt = firstItem(receiptListOf(scope));
468
+ return rawNumberPresent(oracle, ["markPrice", "mark", "priceUsd", "oraclePriceUsd", "price"]) ||
469
+ rawNumberPresent(book, ["markPrice", "mark", "midPrice", "mid", "priceUsd"]) ||
470
+ rawNumberPresent(firstReceipt, ["markPriceUsd", "markPrice", "mark"]) ||
471
+ Number(market.price?.mark) > 0;
472
+ }
473
+
474
+ function hasOracleAgeSignal({ scope, market }) {
475
+ const oracle = objectOf(scope, ["oracle", "price", "prices", "oraclePrice"], {});
476
+ const firstReceipt = firstItem(receiptListOf(scope));
477
+ const age = Number(market.price?.publishAgeSec);
478
+ return rawNumberPresent(oracle, ["publishAgeSec", "ageSecs", "ageSec", "age"]) ||
479
+ rawNumberPresent(firstReceipt, ["oracleAgeSec", "priceAgeSec", "ageSecs"]) ||
480
+ (Number.isFinite(age) && age > 0);
481
+ }
482
+
483
+ function hasEngineSignal({ scope, market }) {
484
+ return nonEmptyObject(objectOf(scope, ["engine", "state", "riskState", "slabEngine", "slab:engine"], {})) ||
485
+ nonEmptyObject(objectOf(scope, ["bitmap", "slabBitmap", "slab:bitmap"], {})) ||
486
+ Number(market.crank?.activeAccounts || 0) > 0 ||
487
+ Number(market.crank?.ageSlots || 0) > 0;
488
+ }
489
+
490
+ function hasCrankAgeSignal({ scope, market }) {
491
+ const engine = objectOf(scope, ["engine", "state", "riskState", "slabEngine", "slab:engine"], {});
492
+ return rawNumberPresent(engine, ["crankAgeSlots", "crank_age_slots", "ageSlots", "lastCrankSlot", "last_crank_slot", "lastMarketSlot", "last_market_slot"]) ||
493
+ Number(market.crank?.ageSlots) > 0;
494
+ }
495
+
496
+ function hasFundingSignal({ scope, market }) {
497
+ const engine = objectOf(scope, ["engine", "state", "riskState", "slabEngine", "slab:engine"], {});
498
+ const firstHistory = firstItem(historyListOf(scope));
499
+ const funding = Number(market.funding?.bpsPerHour);
500
+ return rawNumberPresent(engine, ["fundingRateBpsPerHour", "funding_bps_per_hour", "fundingRate"]) ||
501
+ rawNumberPresent(firstHistory, ["fundingBpsPerHour", "fundingRateBpsPerHour"]) ||
502
+ (Number.isFinite(funding) && funding !== 0);
503
+ }
504
+
505
+ function hasMarketStructureSignal({ scope, market }) {
506
+ const engine = objectOf(scope, ["engine", "state", "riskState", "slabEngine", "slab:engine"], {});
507
+ const marketInfo = objectOf(scope, ["market", "marketInfo", "metadata", "instrument"], {});
508
+ const firstHistory = firstItem(historyListOf(scope));
509
+ return rawNumberPresent(engine, ["openInterestUsd", "open_interest_usd", "oiUsd", "longOpenInterestUsd", "shortOpenInterestUsd"]) ||
510
+ rawNumberPresent(marketInfo, ["openInterestUsd"]) ||
511
+ rawNumberPresent(firstHistory, ["openInterestUsd", "oiUsd", "longOpenInterestUsd", "shortOpenInterestUsd", "oiSkewPct"]) ||
512
+ Number(market.marketStructure?.openInterestUsd) > 0;
513
+ }
514
+
515
+ function hasAccountSignal({ scope, market }) {
516
+ const account = objectOf(scope, ["account", "position", "traderAccount"], {});
517
+ return nonEmptyObject(account) ||
518
+ rowsOf(valueOf(scope, ["accounts", "positions"])).length > 0 ||
519
+ Math.abs(Number(market.account?.positionSize || 0)) > 0 ||
520
+ Number(market.account?.collateralUsd || 0) > 0;
521
+ }
522
+
523
+ function hasPositionNotionalSignal({ scope, market }) {
524
+ const account = objectOf(scope, ["account", "position", "traderAccount"], {});
525
+ return rawNumberPresent(account, ["positionNotionalUsd", "notionalUsd", "notional"]) ||
526
+ (
527
+ rawNumberPresent(account, ["positionSize", "basePosition", "positionSizeBase", "size", "position"]) &&
528
+ Number(market.price?.mark) > 0
529
+ ) ||
530
+ Number(market.account?.positionNotionalUsd) > 0;
531
+ }
532
+
533
+ function hasExecutionSignal({ scope, market }) {
534
+ const book = objectOf(scope, ["bestPrice", "best-price", "best_price", "book", "orderbook", "quote"], {});
535
+ const execution = objectOf(scope, ["execution", "executionQuality"], {});
536
+ const firstReceipt = firstItem(receiptListOf(scope));
537
+ const sourceExecution = objectOf(sourceMarketOf(scope), ["execution", "executionQuality"], {});
538
+ return rawNumberPresent(book, ["bestBid", "bid", "best_bid", "bestAsk", "ask", "best_ask"]) ||
539
+ rawNumberPresent(execution, ["bestBid", "bestAsk"]) ||
540
+ rawNumberPresent(sourceExecution, ["bestBid", "bestAsk"]) ||
541
+ rawNumberPresent(firstReceipt, ["bestBid", "bestAsk", "bid", "ask"]) ||
542
+ (Number(market.execution?.bestBid) > 0 && Number(market.execution?.bestAsk) > 0 && (receiptCount(scope, market) > 0 || nonEmptyObject(sourceExecution)));
543
+ }
544
+
545
+ function hasReceiptSignal({ scope, market }) {
546
+ return receiptCount(scope, market) > 0;
547
+ }
548
+
549
+ function hasHistorySignal({ scope, market }) {
550
+ return historyCount(scope, market) > 0;
551
+ }
552
+
553
+ function receiptCount(scope, market) {
554
+ const sourceReceipts = objectOf(sourceMarketOf(scope), ["execution", "executionQuality"], {});
555
+ return Math.max(
556
+ receiptListOf(scope).length,
557
+ receiptListOf(sourceReceipts).length,
558
+ Array.isArray(market.execution?.receipts) ? market.execution.receipts.length : 0
559
+ );
560
+ }
561
+
562
+ function historyCount(scope, market) {
563
+ const sourceHistory = objectOf(sourceMarketOf(scope), ["history"], {});
564
+ return Math.max(
565
+ historyListOf(scope).length,
566
+ historyListOf(sourceHistory).length,
567
+ Array.isArray(market.history?.fundingSkew) ? market.history.fundingSkew.length : 0
568
+ );
569
+ }
570
+
571
+ function sourceMarketOf(scope) {
572
+ const [market] = marketListOf(scope);
573
+ if (market && typeof market === "object") return market;
574
+ return objectOf(scope, ["market", "marketInfo", "metadata", "instrument"], {});
575
+ }
576
+
577
+ function hasProvenanceSignal({ input, scope, snapshot }) {
578
+ return Array.isArray(snapshot.source?.commandSet) && snapshot.source.commandSet.length > 0 ||
579
+ knownText(valueOf(scope, ["label", "sourceLabel"])) ||
580
+ (knownText(snapshot.cluster) && snapshot.cluster !== "unknown") ||
581
+ rawNumberPresent(scope, ["currentSlot", "slot", "current_slot"]) ||
582
+ commandNames(input).length > 0;
583
+ }
584
+
585
+ function rawNumberPresent(source, aliases) {
586
+ if (!source || typeof source !== "object") return false;
587
+ const value = valueOf(source, aliases);
588
+ if (value === undefined || value === null || value === "") return false;
589
+ const next = Number(typeof value === "string" ? value.replace(/[$,%_\s,]/g, "") : value);
590
+ return Number.isFinite(next);
591
+ }
592
+
593
+ function knownText(value) {
594
+ return Boolean(value && String(value).trim());
595
+ }
596
+
597
+ function nonEmptyObject(value) {
598
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length);
599
+ }
600
+
601
+ function coercePercolatorSnapshot(input) {
602
+ if (!input || typeof input !== "object") {
603
+ return { source: { label: "empty input", mode: "read-only" }, markets: [] };
604
+ }
605
+
606
+ if (Array.isArray(input)) {
607
+ return {
608
+ source: { label: "market array", mode: "read-only" },
609
+ cluster: "unknown",
610
+ currentSlot: 0,
611
+ markets: input.map((market) => coercePercolatorMarket(market))
612
+ };
613
+ }
614
+
615
+ const inputShape = detectPercolatorInputShape(input);
616
+ if (inputShape === "percolator-cli-bundle" || inputShape === "funding-skew-history") {
617
+ return coercePercolatorCliBundle(input);
618
+ }
619
+
620
+ if (Array.isArray(input.markets)) {
621
+ return {
622
+ ...input,
623
+ markets: input.markets.map((market) => coercePercolatorMarket(market, input.currentSlot))
624
+ };
625
+ }
626
+
627
+ return {
628
+ source: { label: "single market import", mode: "read-only" },
629
+ cluster: stringOf(input, ["cluster"], "unknown"),
630
+ currentSlot: numberOf(input, ["currentSlot", "slot"], 0),
631
+ markets: [coercePercolatorMarket(input, input.currentSlot)]
632
+ };
633
+ }
634
+
635
+ function coercePercolatorMarket(market, currentSlot = 0) {
636
+ if (!market || typeof market !== "object") return {};
637
+ if (market.oracle && market.engine && market.account && market.execution) return market;
638
+ const marketInfo = objectOf(market, ["market", "marketInfo", "metadata", "instrument"], market);
639
+ return coercePercolatorCliBundle({
640
+ ...market,
641
+ cluster: market.cluster,
642
+ currentSlot,
643
+ market: marketInfo
644
+ }).markets[0];
645
+ }
646
+
647
+ function coercePercolatorCliBundle(bundle) {
648
+ const scope = collectCliSections(bundle);
649
+ const engineScope = {
650
+ ...objectOf(scope, ["engine", "state", "riskState"], {}),
651
+ ...objectOf(scope, ["slabEngine", "slab:engine"], {})
652
+ };
653
+ const currentSlot = firstNumber(
654
+ maybeNumberOf(scope, ["currentSlot", "slot", "current_slot"]),
655
+ maybeNumberOf(engineScope, ["currentSlot", "current_slot"])
656
+ );
657
+ const markets = marketListOf(scope);
658
+ return {
659
+ source: {
660
+ label: stringOf(scope, ["label", "sourceLabel"], "Percolator CLI bundle"),
661
+ mode: "read-only",
662
+ commandSet: commandNames(bundle)
663
+ },
664
+ cluster: stringOf(scope, ["cluster", "network"], "unknown"),
665
+ currentSlot,
666
+ markets: markets.length
667
+ ? markets.map((market, index) => coerceCliMarket(scopedMarket(scope, market, index), currentSlot))
668
+ : [coerceCliMarket(scope, currentSlot)]
669
+ };
670
+ }
671
+
672
+ function coerceCliMarket(scope, currentSlot) {
673
+ const marketInfo = objectOf(scope, ["market", "marketInfo", "metadata", "instrument"], scope);
674
+ const header = objectOf(scope, ["slabHeader", "slab:header", "header"], {});
675
+ const config = objectOf(scope, ["slabConfig", "slab:config", "config", "marketConfig"], {});
676
+ const accountRows = valueOf(scope, ["accounts", "positions"]);
677
+ const accountStats = summarizeAccounts(accountRows);
678
+ const bitmapStats = summarizeBitmap(objectOf(scope, ["bitmap", "slabBitmap", "slab:bitmap"], {}));
679
+ const engine = {
680
+ ...objectOf(scope, ["engine", "state", "riskState"], {}),
681
+ ...objectOf(scope, ["slabEngine", "slab:engine"], {}),
682
+ ...accountStats,
683
+ ...bitmapStats
684
+ };
685
+ const oracle = objectOf(scope, ["oracle", "price", "prices", "oraclePrice"], {});
686
+ const book = objectOf(scope, ["bestPrice", "best-price", "best_price", "book", "orderbook", "quote"], {});
687
+ const execution = objectOf(scope, ["execution", "executionQuality"], {});
688
+ const historyRows = historyListOf(scope);
689
+ const receiptRows = receiptListOf(scope, execution);
690
+ const firstReceipt = firstItem(receiptRows);
691
+ const account = {
692
+ ...firstItem(accountRows),
693
+ ...firstItem(valueOf(scope, ["account", "position", "traderAccount"]))
694
+ };
695
+ const bestBuy = objectOf(book, ["bestBuy", "best_buy"], objectOf(scope, ["bestBuy", "best_buy"], {}));
696
+ const bestSell = objectOf(book, ["bestSell", "best_sell"], objectOf(scope, ["bestSell", "best_sell"], {}));
697
+ const params = objectOf(scope, ["slabParams", "slab:params", "params", "riskParams"], {});
698
+ const symbol = stringOf(marketInfo, ["symbol", "name", "market", "ticker"], "PERP");
699
+ const parsed = parseSymbol(symbol);
700
+ const base = stringOf(marketInfo, ["base", "baseSymbol", "baseAsset"], parsed.base);
701
+ const quote = stringOf(marketInfo, ["quote", "quoteSymbol", "quoteAsset"], parsed.quote);
702
+ const markPrice = firstNumber(
703
+ priceNumberOf(oracle, ["markPrice", "mark", "priceUsd", "oraclePriceUsd"], ["price", "oraclePrice"]),
704
+ priceNumberOf(book, ["markPrice", "mark", "midPrice", "mid", "priceUsd"], ["price"]),
705
+ priceNumberOf(firstReceipt, ["markPriceUsd", "markPrice", "mark"], ["markPrice"]),
706
+ priceNumberOf(engine, ["lastOraclePriceUsd", "resolvedPriceUsd"], ["lastOraclePrice", "resolvedPrice"]),
707
+ priceNumberOf(marketInfo, ["markPrice", "priceUsd"], ["price"])
708
+ );
709
+ const indexPrice = firstNumber(
710
+ priceNumberOf(oracle, ["indexPrice", "index", "oraclePriceUsd", "priceUsd"], ["oraclePrice", "price"]),
711
+ priceNumberOf(book, ["indexPrice", "oraclePriceUsd"], ["oraclePrice", "price"]),
712
+ markPrice
713
+ );
714
+ const bestBid = firstNumber(
715
+ priceNumberOf(book, ["bestBid", "bid", "best_bid"], []),
716
+ priceNumberOf(execution, ["bestBid"], []),
717
+ priceNumberOf(firstReceipt, ["bestBid", "bid", "best_bid"], []),
718
+ priceNumberOf(bestSell, ["priceUsd"], ["price"])
719
+ );
720
+ const bestAsk = firstNumber(
721
+ priceNumberOf(book, ["bestAsk", "ask", "best_ask"], []),
722
+ priceNumberOf(execution, ["bestAsk"], []),
723
+ priceNumberOf(firstReceipt, ["bestAsk", "ask", "best_ask"], []),
724
+ priceNumberOf(bestBuy, ["priceUsd"], ["price"])
725
+ );
726
+ const spreadFallback = markPrice ? markPrice * 0.0008 : 0;
727
+ const normalizedBestBid = bestBid || Math.max(markPrice - spreadFallback, 0);
728
+ const normalizedBestAsk = bestAsk || markPrice + spreadFallback;
729
+ const positionSize = numberOf(account, ["positionSize", "basePosition", "positionSizeBase", "size", "position"], 0);
730
+ const side = stringOf(account, ["side"], positionSize < 0 ? "short" : positionSize > 0 ? "long" : "flat");
731
+ const positionNotionalUsd = firstNumber(
732
+ maybeNumberOf(account, ["positionNotionalUsd", "notionalUsd", "notional"]),
733
+ Math.abs(positionSize) * markPrice
734
+ );
735
+ const collateralUsd = numberOf(account, ["collateralUsd", "equityCollateralUsd"], 0);
736
+ const unrealizedPnlUsd = numberOf(account, ["unrealizedPnlUsd", "unrealizedPnl", "uPnl"], 0);
737
+ const fundingPnlUsd = numberOf(account, ["fundingPnlUsd", "fundingPnl"], 0);
738
+ const maintenanceMarginUsd = firstNumber(
739
+ maybeNumberOf(account, ["maintenanceMarginUsd", "maintenanceMargin"]),
740
+ positionNotionalUsd * (firstNumber(
741
+ maybeNumberOf(params, ["maintenanceMarginBps", "maintenance_margin_bps"]),
742
+ maybeNumberOf(config, ["maintenanceMarginBps", "maintenance_margin_bps"]),
743
+ 500
744
+ ) / 10000)
745
+ );
746
+ const liquidationPrice = firstNumber(
747
+ maybeNumberOf(account, ["liquidationPrice", "liqPrice", "liquidation_price"]),
748
+ markPrice * (side === "short" ? 1.18 : 0.82)
749
+ );
750
+ const lastCrankSlot = numberOf(engine, ["lastCrankSlot", "last_crank_slot"], currentSlot);
751
+ const crankAgeSlots = firstNumber(
752
+ maybeNumberOf(engine, ["crankAgeSlots", "crank_age_slots", "ageSlots"]),
753
+ currentSlot && numberOf(engine, ["lastMarketSlot", "last_market_slot"]) ? currentSlot - numberOf(engine, ["lastMarketSlot", "last_market_slot"]) : undefined,
754
+ Math.max(currentSlot - lastCrankSlot, 0)
755
+ );
756
+ const stressConsumedBps = firstNumber(
757
+ maybeNumberOf(engine, ["stressConsumedBps", "stress_consumed_bps"]),
758
+ maybeNumberOf(engine, ["stressConsumedBpsE9SinceEnvelope"]) / 1e9
759
+ );
760
+ const openInterestUsd = firstNumber(
761
+ maybeNumberOf(engine, ["openInterestUsd", "open_interest_usd", "oiUsd"]),
762
+ maybeNumberOf(marketInfo, ["openInterestUsd"])
763
+ );
764
+ const longOpenInterestUsd = firstNumber(
765
+ maybeNumberOf(engine, ["longOpenInterestUsd", "long_oi_usd"]),
766
+ maybeNumberOf(marketInfo, ["longOpenInterestUsd"])
767
+ );
768
+ const shortOpenInterestUsd = firstNumber(
769
+ maybeNumberOf(engine, ["shortOpenInterestUsd", "short_oi_usd"]),
770
+ maybeNumberOf(marketInfo, ["shortOpenInterestUsd"])
771
+ );
772
+ const insuranceUsd = firstNumber(
773
+ maybeNumberOf(engine, ["insuranceUsd", "insurance_usd"]),
774
+ maybeNumberOf(marketInfo, ["insuranceUsd"])
775
+ );
776
+
777
+ return {
778
+ id: stringOf(marketInfo, ["id", "marketId"], `${base.toLowerCase()}-perp`),
779
+ name: stringOf(marketInfo, ["name", "symbol"], `${base}-PERP`),
780
+ base,
781
+ quote,
782
+ status: stringOf(marketInfo, ["status"], "read-only"),
783
+ slab: stringOf(marketInfo, ["slab", "slabAddress", "address", "pubkey"], "unknown slab"),
784
+ program: stringOf(marketInfo, ["program", "programId", "owner"], "unknown program"),
785
+ header,
786
+ config: {
787
+ maxLeverage: numberOf(config, ["maxLeverage", "max_leverage"], 0),
788
+ initialMarginBps: firstNumber(maybeNumberOf(params, ["initialMarginBps", "initial_margin_bps"]), maybeNumberOf(config, ["initialMarginBps", "initial_margin_bps"])),
789
+ maintenanceMarginBps: firstNumber(maybeNumberOf(params, ["maintenanceMarginBps", "maintenance_margin_bps"]), maybeNumberOf(config, ["maintenanceMarginBps", "maintenance_margin_bps"])),
790
+ liquidationFeeBps: firstNumber(maybeNumberOf(params, ["liquidationFeeBps", "liquidation_fee_bps"]), maybeNumberOf(config, ["liquidationFeeBps", "liquidation_fee_bps"])),
791
+ fundingMaxPremiumBps: numberOf(config, ["fundingMaxPremiumBps", "funding_max_premium_bps"], 0),
792
+ maxStalenessSecs: numberOf(config, ["maxStalenessSecs", "max_staleness_secs", "maxOracleAgeSec"], 8),
793
+ confFilterBps: numberOf(config, ["confFilterBps", "confidenceFilterBps"], 0),
794
+ permissionlessResolveStaleSlots: numberOf(config, ["permissionlessResolveStaleSlots"], 0),
795
+ forceCloseDelaySlots: numberOf(config, ["forceCloseDelaySlots"], 0)
796
+ },
797
+ oracle: {
798
+ indexPrice,
799
+ markPrice,
800
+ effectivePrice: firstNumber(priceNumberOf(oracle, ["effectivePrice", "effectivePriceUsd"], []), markPrice),
801
+ confidenceBps: numberOf(oracle, ["confidenceBps", "confidence_bps", "confBps"], 0),
802
+ publishAgeSec: numberOf(oracle, ["publishAgeSec", "ageSecs", "ageSec", "age"], 0),
803
+ pricePath: arrayOf(oracle, ["pricePath", "path", "history"], [indexPrice, markPrice]),
804
+ legs: arrayOf(oracle, ["legs", "sources"], [])
805
+ },
806
+ engine: {
807
+ lastCrankSlot,
808
+ crankAgeSlots,
809
+ catchupRequired: Boolean(valueOf(engine, ["catchupRequired", "catchup_required"])),
810
+ staleAccounts: numberOf(engine, ["staleAccounts", "stale_accounts", "staleAccountCount"], 0),
811
+ activeAccounts: firstNumber(
812
+ maybeNumberOf(engine, ["activeAccounts", "active_accounts", "materializedAccountCount"]),
813
+ maybeNumberOf(engine, ["numUsedAccounts", "numUsed", "usedAccounts"])
814
+ ),
815
+ maxAccounts: firstNumber(
816
+ maybeNumberOf(engine, ["maxAccounts", "max_accounts"]),
817
+ maybeNumberOf(params, ["maxAccounts", "max_accounts"]),
818
+ maybeNumberOf(config, ["maxAccounts", "max_accounts"])
819
+ ),
820
+ fundingRateBpsPerHour: numberOf(engine, ["fundingRateBpsPerHour", "funding_bps_per_hour", "fundingRate"], 0),
821
+ fundingIndex: valueOf(engine, ["fundingIndex", "funding_index"]) || "0",
822
+ openInterestUsd,
823
+ longOpenInterestUsd,
824
+ shortOpenInterestUsd,
825
+ stressConsumedBps,
826
+ stressLimitBps: numberOf(engine, ["stressLimitBps", "stress_limit_bps"], 500),
827
+ insuranceUsd,
828
+ vaultUsd: numberOf(engine, ["vaultUsd", "vault_usd"], 0),
829
+ claimUsd: numberOf(engine, ["claimUsd", "claim_usd"], 0),
830
+ socialLossUsd: numberOf(engine, ["socialLossUsd", "social_loss_usd"], 0),
831
+ sideMode: stringOf(engine, ["sideMode", "side_mode"], "unknown")
832
+ },
833
+ account: {
834
+ label: stringOf(account, ["label", "ownerLabel", "name"], "Read-only observer"),
835
+ side,
836
+ positionSize,
837
+ positionNotionalUsd,
838
+ collateralUsd,
839
+ unrealizedPnlUsd,
840
+ realizedPnlUsd: numberOf(account, ["realizedPnlUsd", "realizedPnl"], 0),
841
+ fundingPnlUsd,
842
+ maintenanceMarginUsd,
843
+ initialMarginUsd: firstNumber(
844
+ maybeNumberOf(account, ["initialMarginUsd", "initialMargin"]),
845
+ positionNotionalUsd * (firstNumber(
846
+ maybeNumberOf(params, ["initialMarginBps", "initial_margin_bps"]),
847
+ maybeNumberOf(config, ["initialMarginBps", "initial_margin_bps"]),
848
+ 800
849
+ ) / 10000)
850
+ ),
851
+ liquidationPrice,
852
+ pnlPath: arrayOf(account, ["pnlPath", "pnlHistory"], [unrealizedPnlUsd])
853
+ },
854
+ execution: {
855
+ bestBid: normalizedBestBid,
856
+ bestAsk: normalizedBestAsk,
857
+ impact10kBps: firstNumber(
858
+ maybeNumberOf(execution, ["impact10kBps", "impact_10k_bps"]),
859
+ maybeNumberOf(firstReceipt, ["impactBps", "priceImpactBps", "impact_bps"]),
860
+ maybeNumberOf(book, ["effectiveSpreadBps"])
861
+ ),
862
+ impact50kBps: firstNumber(
863
+ maybeNumberOf(execution, ["impact50kBps", "impact_50k_bps"]),
864
+ maybeNumberOf(firstReceipt, ["impact50kBps", "impact_50k_bps"])
865
+ ),
866
+ markout1mBps: firstNumber(
867
+ maybeNumberOf(execution, ["markout1mBps", "markout_1m_bps"]),
868
+ maybeNumberOf(firstReceipt, ["markout1mBps", "markout_1m_bps", "markout60sBps"])
869
+ ),
870
+ markout5mBps: firstNumber(
871
+ maybeNumberOf(execution, ["markout5mBps", "markout_5m_bps"]),
872
+ maybeNumberOf(firstReceipt, ["markout5mBps", "markout_5m_bps", "markout300sBps"])
873
+ ),
874
+ fillQualityScore: firstNumber(
875
+ maybeNumberOf(execution, ["fillQualityScore", "fill_quality_score"]),
876
+ maybeNumberOf(firstReceipt, ["fillQualityScore", "qualityScore"]),
877
+ 72
878
+ ),
879
+ routeLatencyMs: firstNumber(
880
+ maybeNumberOf(execution, ["routeLatencyMs", "latencyMs"]),
881
+ maybeNumberOf(firstReceipt, ["routeLatencyMs", "latencyMs", "durationMs"])
882
+ ),
883
+ priorityFeeMicrolamports: firstNumber(
884
+ maybeNumberOf(execution, ["priorityFeeMicrolamports", "priorityFee"]),
885
+ maybeNumberOf(firstReceipt, ["priorityFeeMicrolamports", "priorityFee", "priorityFeeMicroLamports"])
886
+ ),
887
+ receipts: normalizeExecutionReceipts(receiptRows, {
888
+ ...execution,
889
+ bestBid: normalizedBestBid,
890
+ bestAsk: normalizedBestAsk,
891
+ spreadBps: bps(normalizedBestAsk - normalizedBestBid, midpoint(normalizedBestAsk, normalizedBestBid))
892
+ })
893
+ },
894
+ history: {
895
+ fundingSkew: historyRows
896
+ }
897
+ };
898
+ }
899
+
900
+ export function toTerminalMarketDto(market, currentSlot = 0) {
901
+ const price = number(market.oracle?.markPrice);
902
+ const indexPrice = number(market.oracle?.indexPrice);
903
+ const effectivePrice = number(market.oracle?.effectivePrice || price);
904
+ const account = market.account || {};
905
+ const engine = market.engine || {};
906
+ const config = market.config || {};
907
+ const execution = market.execution || {};
908
+ const executionReceipts = normalizeExecutionReceipts(execution.receipts || execution.receiptTimeline || [], execution);
909
+
910
+ const spreadBps = bps(execution.bestAsk - execution.bestBid, midpoint(execution.bestAsk, execution.bestBid));
911
+ const markDriftBps = bps(price - indexPrice, indexPrice);
912
+ const oracleFreshness = freshnessScore(market.oracle?.publishAgeSec, config.maxStalenessSecs);
913
+ const crankFreshness = freshnessScore(engine.crankAgeSlots, 220);
914
+ const stressUsedPct = percent(engine.stressConsumedBps, engine.stressLimitBps);
915
+ const insuranceCoveragePct = percent(engine.insuranceUsd, Math.max(engine.claimUsd || 1, 1));
916
+ const oiSkewPct = percent(engine.longOpenInterestUsd - engine.shortOpenInterestUsd, engine.openInterestUsd);
917
+ const equityUsd = number(account.collateralUsd) + number(account.unrealizedPnlUsd) + number(account.fundingPnlUsd);
918
+ const marginBufferUsd = equityUsd - number(account.maintenanceMarginUsd);
919
+ const marginBufferPct = percent(marginBufferUsd, Math.max(number(account.positionNotionalUsd), 1));
920
+ const liquidationDistancePct = liquidationDistance({
921
+ price,
922
+ liquidationPrice: number(account.liquidationPrice),
923
+ side: account.side
924
+ });
925
+ const dailyFundingUsd = number(account.positionNotionalUsd) * (number(engine.fundingRateBpsPerHour) / 10000) * 24;
926
+ const signedDailyFundingUsd = account.side === "short" ? -dailyFundingUsd : dailyFundingUsd;
927
+ const executionScore = clamp(number(execution.fillQualityScore), 0, 100);
928
+ const rawFundingSkewHistory = rowsOf(market.history?.fundingSkew || market.history);
929
+ const fundingSkewHistory = rawFundingSkewHistory.length
930
+ ? normalizeFundingSkewHistory(rawFundingSkewHistory, {
931
+ currentSlot,
932
+ sourceStatus: market.status,
933
+ funding: { bpsPerHour: number(engine.fundingRateBpsPerHour) },
934
+ marketStructure: {
935
+ openInterestUsd: number(engine.openInterestUsd),
936
+ longOpenInterestUsd: number(engine.longOpenInterestUsd),
937
+ shortOpenInterestUsd: number(engine.shortOpenInterestUsd),
938
+ oiSkewPct,
939
+ stressUsedPct
940
+ },
941
+ price: {
942
+ publishAgeSec: number(market.oracle?.publishAgeSec)
943
+ }
944
+ })
945
+ : [];
946
+
947
+ const healthScore = weightedScore([
948
+ [liquidationDistancePct * 6, 0.26],
949
+ [marginBufferPct * 9, 0.18],
950
+ [oracleFreshness, 0.16],
951
+ [crankFreshness, 0.13],
952
+ [100 - stressUsedPct, 0.13],
953
+ [Math.min(insuranceCoveragePct, 120) * 0.82, 0.08],
954
+ [executionScore, 0.06]
955
+ ]);
956
+
957
+ const flags = buildFlags({
958
+ market,
959
+ oracleFreshness,
960
+ crankFreshness,
961
+ stressUsedPct,
962
+ insuranceCoveragePct,
963
+ liquidationDistancePct,
964
+ marginBufferUsd,
965
+ spreadBps
966
+ });
967
+
968
+ return {
969
+ id: market.id,
970
+ name: market.name,
971
+ base: market.base,
972
+ quote: market.quote,
973
+ status: labelFromScore(healthScore),
974
+ sourceStatus: market.status,
975
+ slab: market.slab,
976
+ program: market.program,
977
+ header: market.header,
978
+ config,
979
+ currentSlot,
980
+ price: {
981
+ index: indexPrice,
982
+ mark: price,
983
+ effective: effectivePrice,
984
+ driftBps: markDriftBps,
985
+ confidenceBps: number(market.oracle?.confidenceBps),
986
+ publishAgeSec: number(market.oracle?.publishAgeSec),
987
+ freshnessScore: oracleFreshness,
988
+ path: market.oracle?.pricePath || [],
989
+ legs: market.oracle?.legs || []
990
+ },
991
+ crank: {
992
+ lastSlot: engine.lastCrankSlot,
993
+ ageSlots: number(engine.crankAgeSlots),
994
+ freshnessScore: crankFreshness,
995
+ catchupRequired: Boolean(engine.catchupRequired),
996
+ staleAccounts: number(engine.staleAccounts),
997
+ activeAccounts: number(engine.activeAccounts),
998
+ maxAccounts: number(engine.maxAccounts)
999
+ },
1000
+ funding: {
1001
+ bpsPerHour: number(engine.fundingRateBpsPerHour),
1002
+ dailyUsd: signedDailyFundingUsd,
1003
+ index: engine.fundingIndex
1004
+ },
1005
+ marketStructure: {
1006
+ openInterestUsd: number(engine.openInterestUsd),
1007
+ longOpenInterestUsd: number(engine.longOpenInterestUsd),
1008
+ shortOpenInterestUsd: number(engine.shortOpenInterestUsd),
1009
+ oiSkewPct,
1010
+ stressUsedPct,
1011
+ sideMode: engine.sideMode
1012
+ },
1013
+ solvency: {
1014
+ insuranceUsd: number(engine.insuranceUsd),
1015
+ vaultUsd: number(engine.vaultUsd),
1016
+ claimUsd: number(engine.claimUsd),
1017
+ socialLossUsd: number(engine.socialLossUsd),
1018
+ coveragePct: insuranceCoveragePct
1019
+ },
1020
+ account: {
1021
+ label: account.label,
1022
+ side: account.side,
1023
+ positionSize: number(account.positionSize),
1024
+ positionNotionalUsd: number(account.positionNotionalUsd),
1025
+ collateralUsd: number(account.collateralUsd),
1026
+ equityUsd,
1027
+ unrealizedPnlUsd: number(account.unrealizedPnlUsd),
1028
+ realizedPnlUsd: number(account.realizedPnlUsd),
1029
+ fundingPnlUsd: number(account.fundingPnlUsd),
1030
+ maintenanceMarginUsd: number(account.maintenanceMarginUsd),
1031
+ initialMarginUsd: number(account.initialMarginUsd),
1032
+ liquidationPrice: number(account.liquidationPrice),
1033
+ liquidationDistancePct,
1034
+ marginBufferUsd,
1035
+ marginBufferPct,
1036
+ pnlPath: account.pnlPath || []
1037
+ },
1038
+ execution: {
1039
+ bestBid: number(execution.bestBid),
1040
+ bestAsk: number(execution.bestAsk),
1041
+ spreadBps,
1042
+ impact10kBps: number(execution.impact10kBps),
1043
+ impact50kBps: number(execution.impact50kBps),
1044
+ markout1mBps: number(execution.markout1mBps),
1045
+ markout5mBps: number(execution.markout5mBps),
1046
+ fillQualityScore: executionScore,
1047
+ routeLatencyMs: number(execution.routeLatencyMs),
1048
+ priorityFeeMicrolamports: number(execution.priorityFeeMicrolamports),
1049
+ receipts: executionReceipts
1050
+ },
1051
+ history: {
1052
+ fundingSkew: fundingSkewHistory
1053
+ },
1054
+ flags,
1055
+ healthScore
1056
+ };
1057
+ }
1058
+
1059
+ export function simulatePriceShock(marketDto, shockPct) {
1060
+ const nextPrice = marketDto.price.mark * (1 + number(shockPct) / 100);
1061
+ const signedSize = Math.abs(marketDto.account.positionSize) * (marketDto.account.side === "short" ? -1 : 1);
1062
+ const priceMove = nextPrice - marketDto.price.mark;
1063
+ const projectedPnl = marketDto.account.unrealizedPnlUsd + signedSize * priceMove;
1064
+ const projectedEquity =
1065
+ marketDto.account.collateralUsd + projectedPnl + marketDto.account.fundingPnlUsd;
1066
+ const projectedBufferUsd = projectedEquity - marketDto.account.maintenanceMarginUsd;
1067
+ const projectedLiqDistancePct = liquidationDistance({
1068
+ price: nextPrice,
1069
+ liquidationPrice: marketDto.account.liquidationPrice,
1070
+ side: marketDto.account.side
1071
+ });
1072
+ const projectedScore = clamp(
1073
+ weightedScore([
1074
+ [projectedLiqDistancePct * 6, 0.48],
1075
+ [percent(projectedBufferUsd, Math.max(marketDto.account.positionNotionalUsd, 1)) * 9, 0.28],
1076
+ [marketDto.price.freshnessScore, 0.12],
1077
+ [100 - marketDto.marketStructure.stressUsedPct, 0.12]
1078
+ ]),
1079
+ 0,
1080
+ 100
1081
+ );
1082
+
1083
+ return {
1084
+ shockPct: number(shockPct),
1085
+ nextPrice,
1086
+ projectedPnl,
1087
+ projectedEquity,
1088
+ projectedBufferUsd,
1089
+ projectedLiqDistancePct,
1090
+ projectedScore,
1091
+ projectedStatus: labelFromScore(projectedScore)
1092
+ };
1093
+ }
1094
+
1095
+ function buildFlags(context) {
1096
+ const flags = [];
1097
+ if (context.liquidationDistancePct < 4) flags.push({ tone: "danger", label: "liq band tight" });
1098
+ if (context.marginBufferUsd < 0) flags.push({ tone: "danger", label: "below maintenance" });
1099
+ if (context.oracleFreshness < 55) flags.push({ tone: "danger", label: "oracle stale" });
1100
+ if (context.crankFreshness < 45) flags.push({ tone: "danger", label: "crank lag" });
1101
+ if (context.stressUsedPct > 75) flags.push({ tone: "danger", label: "stress cap hot" });
1102
+ if (context.insuranceCoveragePct < 100) flags.push({ tone: "warning", label: "insurance thin" });
1103
+ if (context.spreadBps > 35) flags.push({ tone: "warning", label: "wide spread" });
1104
+ if (context.market.engine?.catchupRequired) flags.push({ tone: "danger", label: "catchup required" });
1105
+ if (!flags.length) flags.push({ tone: "good", label: "read-only healthy" });
1106
+ return flags.slice(0, 4);
1107
+ }
1108
+
1109
+ function liquidationDistance({ price, liquidationPrice, side }) {
1110
+ if (!price || !liquidationPrice) return 0;
1111
+ const raw = side === "short"
1112
+ ? (liquidationPrice - price) / price
1113
+ : (price - liquidationPrice) / price;
1114
+ return clamp(raw * 100, -100, 100);
1115
+ }
1116
+
1117
+ function freshnessScore(age, maxAge) {
1118
+ const normalizedAge = number(age);
1119
+ const normalizedMax = Math.max(number(maxAge), 1);
1120
+ return clamp(100 - (normalizedAge / normalizedMax) * 100, 0, 100);
1121
+ }
1122
+
1123
+ function weightedScore(parts) {
1124
+ const totalWeight = parts.reduce((sum, [, weight]) => sum + weight, 0);
1125
+ const total = parts.reduce((sum, [score, weight]) => sum + clamp(number(score), 0, 100) * weight, 0);
1126
+ return Math.round(total / totalWeight);
1127
+ }
1128
+
1129
+ function labelFromScore(score) {
1130
+ if (score >= 72) return "stable";
1131
+ if (score >= 48) return "watch";
1132
+ return "risk";
1133
+ }
1134
+
1135
+ function midpoint(a, b) {
1136
+ return (number(a) + number(b)) / 2;
1137
+ }
1138
+
1139
+ function bps(delta, base) {
1140
+ return base ? (number(delta) / Math.abs(number(base))) * 10000 : 0;
1141
+ }
1142
+
1143
+ function percent(value, base) {
1144
+ return base ? (number(value) / Math.abs(number(base))) * 100 : 0;
1145
+ }
1146
+
1147
+ function average(values) {
1148
+ if (!values.length) return 0;
1149
+ return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
1150
+ }
1151
+
1152
+ function collectCliSections(input) {
1153
+ const sections = {
1154
+ market: input?.market,
1155
+ engine: input?.engine,
1156
+ account: input?.account,
1157
+ accounts: input?.accounts,
1158
+ bitmap: input?.bitmap,
1159
+ receipts: input?.receipts || input?.executionReceipts || input?.receiptTimeline,
1160
+ history: input?.history || input?.fundingHistory || input?.fundingSkewHistory
1161
+ };
1162
+ if (!input || typeof input !== "object") return sections;
1163
+
1164
+ if (
1165
+ input.command &&
1166
+ (
1167
+ input.output !== undefined ||
1168
+ input.data !== undefined ||
1169
+ input.result !== undefined ||
1170
+ input.stdout !== undefined ||
1171
+ input.stdoutText !== undefined
1172
+ )
1173
+ ) {
1174
+ const output = cliOutputOf(input);
1175
+ sections[input.command] = output;
1176
+ mergeNamedCliOutput(sections, input.command, output);
1177
+ mergeCompositeCliOutput(sections, output);
1178
+ }
1179
+
1180
+ for (const entry of input.commands || input.outputs || []) {
1181
+ if (!entry || typeof entry !== "object") continue;
1182
+ const name = entry.command || entry.name || entry.label || entry.kind;
1183
+ const output = cliOutputOf(entry);
1184
+ if (name) sections[name] = output;
1185
+ mergeNamedCliOutput(sections, name, output);
1186
+ mergeCompositeCliOutput(sections, output);
1187
+ }
1188
+
1189
+ return {
1190
+ ...input,
1191
+ ...sections
1192
+ };
1193
+ }
1194
+
1195
+ function mergeCompositeCliOutput(sections, output) {
1196
+ if (!output || typeof output !== "object" || Array.isArray(output)) return;
1197
+ for (const key of [
1198
+ "market",
1199
+ "header",
1200
+ "config",
1201
+ "params",
1202
+ "engine",
1203
+ "oracle",
1204
+ "accounts",
1205
+ "bitmap",
1206
+ "execution",
1207
+ "receipts",
1208
+ "executionReceipts",
1209
+ "receiptTimeline",
1210
+ "history",
1211
+ "fundingHistory",
1212
+ "fundingSkewHistory"
1213
+ ]) {
1214
+ if (output[key] && sections[key] === undefined) sections[key] = output[key];
1215
+ }
1216
+ if (output.slab && sections.market === undefined) sections.market = output;
1217
+ if (output.bestBuy || output.bestSell || output.effectiveSpreadBps !== undefined) {
1218
+ sections.bestPrice = output;
1219
+ }
1220
+ }
1221
+
1222
+ function commandNames(input) {
1223
+ const names = [];
1224
+ if (input?.command) names.push(input.command);
1225
+ for (const entry of input?.commands || input?.outputs || []) {
1226
+ const name = entry?.command || entry?.name || entry?.label || entry?.kind;
1227
+ if (name) names.push(name);
1228
+ }
1229
+ return names;
1230
+ }
1231
+
1232
+ function mergeNamedCliOutput(sections, name, output) {
1233
+ const key = normalizeKey(name || "");
1234
+ if (!key || !output || typeof output !== "object") return;
1235
+ if (key === "slabget") {
1236
+ mergeCompositeCliOutput(sections, output);
1237
+ if (!sections.market && !Array.isArray(output)) sections.market = output.market || output;
1238
+ }
1239
+ if (key === "slabparams") sections.params = output;
1240
+ if (key === "slabengine") sections.engine = { ...(sections.engine || {}), ...output };
1241
+ if (key === "slabaccount") sections.account = { ...(sections.account || {}), ...output };
1242
+ if (key === "slabaccounts") sections.accounts = output;
1243
+ if (key === "slabbitmap") sections.bitmap = output;
1244
+ if (key === "bestprice") sections.bestPrice = output;
1245
+ if (key === "listmarkets") sections.markets = output;
1246
+ if (["executionreceipts", "executionreceipt", "receipts", "receiptlog", "fillreceipts", "fills"].includes(key)) {
1247
+ sections.receipts = output;
1248
+ }
1249
+ if (isHistoryCommandName(key)) {
1250
+ sections.history = output;
1251
+ }
1252
+ }
1253
+
1254
+ function hasCliSections(input) {
1255
+ return Object.keys(input || {}).some((key) =>
1256
+ [
1257
+ "slabHeader",
1258
+ "slabConfig",
1259
+ "slabEngine",
1260
+ "slabBitmap",
1261
+ "bestPrice",
1262
+ "best-price",
1263
+ "marketInfo",
1264
+ "receipts",
1265
+ "executionReceipts",
1266
+ "receiptTimeline",
1267
+ "fillReceipts",
1268
+ "fills",
1269
+ "history",
1270
+ "fundingHistory",
1271
+ "fundingSkewHistory"
1272
+ ].some(
1273
+ (alias) => normalizeKey(alias) === normalizeKey(key)
1274
+ )
1275
+ );
1276
+ }
1277
+
1278
+ function marketListOf(scope) {
1279
+ const value = valueOf(scope, ["markets", "listMarkets", "list-markets"]);
1280
+ if (Array.isArray(value)) return value;
1281
+ if (Array.isArray(value?.markets)) return value.markets;
1282
+ if (Array.isArray(value?.items)) return value.items;
1283
+ if (Array.isArray(value?.rows)) return value.rows;
1284
+ if (Array.isArray(value?.accounts)) return value.accounts;
1285
+ return [];
1286
+ }
1287
+
1288
+ function receiptListOf(...sources) {
1289
+ for (const source of sources) {
1290
+ if (!source) continue;
1291
+ const directRows = rowsOf(source);
1292
+ if (directRows.length && Array.isArray(source)) return directRows;
1293
+ const value = valueOf(source, ["receipts", "executionReceipts", "receiptTimeline", "fillReceipts", "fills"]);
1294
+ const rows = rowsOf(value);
1295
+ if (rows.length) return rows;
1296
+ const nested = valueOf(value, ["receipts", "executionReceipts", "receiptTimeline", "fillReceipts", "fills"]);
1297
+ const nestedRows = rowsOf(nested);
1298
+ if (nestedRows.length) return nestedRows;
1299
+ }
1300
+ return [];
1301
+ }
1302
+
1303
+ function historyListOf(...sources) {
1304
+ for (const source of sources) {
1305
+ if (!source) continue;
1306
+ const directRows = rowsOf(source);
1307
+ if (directRows.length && Array.isArray(source)) return directRows;
1308
+ const value = valueOf(source, ["fundingSkew", "fundingHistory", "fundingSkewHistory", "history"]);
1309
+ const rows = rowsOf(value);
1310
+ if (rows.length) return rows;
1311
+ const nested = valueOf(value, ["fundingSkew", "fundingHistory", "fundingSkewHistory", "history"]);
1312
+ const nestedRows = rowsOf(nested);
1313
+ if (nestedRows.length) return nestedRows;
1314
+ }
1315
+ return [];
1316
+ }
1317
+
1318
+ function isFundingHistoryInput(input) {
1319
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
1320
+ const names = commandNames(input);
1321
+ if (names.length) return names.every(isHistoryCommandName);
1322
+ if (!historyListOf(input).length) return false;
1323
+ const nonHistoryAliases = [
1324
+ "markets",
1325
+ "market",
1326
+ "engine",
1327
+ "account",
1328
+ "accounts",
1329
+ "bitmap",
1330
+ "receipts",
1331
+ "executionReceipts",
1332
+ "receiptTimeline",
1333
+ "slabHeader",
1334
+ "slabConfig",
1335
+ "slabEngine",
1336
+ "slabBitmap",
1337
+ "bestPrice",
1338
+ "marketInfo"
1339
+ ];
1340
+ return !Object.keys(input).some((key) =>
1341
+ nonHistoryAliases.some((alias) => normalizeKey(alias) === normalizeKey(key))
1342
+ );
1343
+ }
1344
+
1345
+ function isFundingHistoryArray(input) {
1346
+ if (!Array.isArray(input) || !input.length) return false;
1347
+ return input.every(isFundingHistoryRow);
1348
+ }
1349
+
1350
+ function isFundingHistoryRow(row) {
1351
+ if (!row || typeof row !== "object" || Array.isArray(row)) return false;
1352
+ return [
1353
+ "fundingBpsPerHour",
1354
+ "fundingRateBpsPerHour",
1355
+ "oiSkewPct",
1356
+ "openInterestUsd",
1357
+ "stressUsedPct",
1358
+ "oracleAgeSec",
1359
+ "sourceTimestamp"
1360
+ ].some((alias) => valueOf(row, [alias]) !== undefined);
1361
+ }
1362
+
1363
+ function isReadOnlyRpcInput(input) {
1364
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
1365
+ const account = objectOf(input, ["account", "accountInfo"], {});
1366
+ if (!nonEmptyObject(account)) return false;
1367
+ return knownText(valueOf(input, ["slab", "slabAddress", "pubkey"])) &&
1368
+ knownText(valueOf(input, ["programId", "program"])) &&
1369
+ (
1370
+ knownText(valueOf(account, ["owner", "programId"])) ||
1371
+ valueOf(account, ["decoded"]) !== undefined ||
1372
+ valueOf(account, ["dataLength", "dataLen", "space"]) !== undefined
1373
+ );
1374
+ }
1375
+
1376
+ function isHistoryCommandName(value) {
1377
+ return HISTORY_COMMAND_KEYS.has(normalizeKey(value || ""));
1378
+ }
1379
+
1380
+ function normalizeExecutionReceipts(rows, fallback = {}) {
1381
+ return rowsOf(rows).slice(0, 24).map((receipt, index) => {
1382
+ const bestBid = firstNumber(
1383
+ priceNumberOf(receipt, ["bestBid", "bid", "best_bid"], []),
1384
+ priceNumberOf(fallback, ["bestBid"], [])
1385
+ );
1386
+ const bestAsk = firstNumber(
1387
+ priceNumberOf(receipt, ["bestAsk", "ask", "best_ask"], []),
1388
+ priceNumberOf(fallback, ["bestAsk"], [])
1389
+ );
1390
+ const quotePrice = firstNumber(
1391
+ priceNumberOf(receipt, ["quotePriceUsd", "quotePrice", "quotedPriceUsd", "quotedPrice"], ["quotePrice"]),
1392
+ priceNumberOf(receipt, ["priceUsd"], ["price"])
1393
+ );
1394
+ const fillPrice = firstNumber(
1395
+ priceNumberOf(receipt, ["fillPriceUsd", "fillPrice", "executionPriceUsd", "executionPrice"], ["fillPrice"]),
1396
+ quotePrice
1397
+ );
1398
+ const markPrice = firstNumber(
1399
+ priceNumberOf(receipt, ["markPriceUsd", "markPrice", "mark"], ["markPrice"]),
1400
+ priceNumberOf(fallback, ["markPrice"], [])
1401
+ );
1402
+ const spreadBps = firstNumber(
1403
+ maybeNumberOf(receipt, ["spreadBps", "spread_bps", "effectiveSpreadBps"]),
1404
+ maybeNumberOf(fallback, ["spreadBps"]),
1405
+ bps(bestAsk - bestBid, midpoint(bestAsk, bestBid))
1406
+ );
1407
+ const impactBps = firstNumber(
1408
+ maybeNumberOf(receipt, ["impactBps", "priceImpactBps", "impact_bps"]),
1409
+ maybeNumberOf(fallback, ["impact10kBps", "impact_10k_bps"])
1410
+ );
1411
+ const markout1mBps = firstNumber(
1412
+ maybeNumberOf(receipt, ["markout1mBps", "markout_1m_bps", "markout60sBps"]),
1413
+ maybeNumberOf(fallback, ["markout1mBps", "markout_1m_bps"])
1414
+ );
1415
+ const markout5mBps = firstNumber(
1416
+ maybeNumberOf(receipt, ["markout5mBps", "markout_5m_bps", "markout300sBps"]),
1417
+ maybeNumberOf(fallback, ["markout5mBps", "markout_5m_bps"])
1418
+ );
1419
+
1420
+ return {
1421
+ id: stringOf(receipt, ["id", "receiptId", "signature", "txid"], `receipt-${index + 1}`),
1422
+ label: stringOf(receipt, ["label", "kind", "venue", "route"], `fill ${index + 1}`),
1423
+ source: stringOf(receipt, ["source", "origin", "adapter"], "adapter"),
1424
+ sourceTimestamp: stringOf(receipt, ["sourceTimestamp", "timestamp", "observedAt", "filledAt", "ts"], ""),
1425
+ slot: numberOf(receipt, ["slot", "sourceSlot", "marketSlot"], 0),
1426
+ side: stringOf(receipt, ["side", "direction"], "fill"),
1427
+ notionalUsd: firstNumber(maybeNumberOf(receipt, ["notionalUsd", "sizeUsd", "quoteNotionalUsd"])),
1428
+ quotePrice,
1429
+ fillPrice,
1430
+ markPrice,
1431
+ bestBid,
1432
+ bestAsk,
1433
+ spreadBps,
1434
+ impactBps,
1435
+ markout1mBps,
1436
+ markout5mBps,
1437
+ routeLatencyMs: firstNumber(
1438
+ maybeNumberOf(receipt, ["routeLatencyMs", "latencyMs", "durationMs"]),
1439
+ maybeNumberOf(fallback, ["routeLatencyMs", "latencyMs"])
1440
+ ),
1441
+ priorityFeeMicrolamports: firstNumber(
1442
+ maybeNumberOf(receipt, ["priorityFeeMicrolamports", "priorityFee", "priorityFeeMicroLamports"]),
1443
+ maybeNumberOf(fallback, ["priorityFeeMicrolamports", "priorityFee"])
1444
+ ),
1445
+ oracleAgeSec: firstNumber(maybeNumberOf(receipt, ["oracleAgeSec", "priceAgeSec", "ageSecs"])),
1446
+ crankAgeSlots: firstNumber(maybeNumberOf(receipt, ["crankAgeSlots", "ageSlots"])),
1447
+ fundingBpsPerHour: firstNumber(maybeNumberOf(receipt, ["fundingBpsPerHour", "fundingRateBpsPerHour"])),
1448
+ fillQualityScore: clamp(
1449
+ firstNumber(maybeNumberOf(receipt, ["fillQualityScore", "qualityScore"]), maybeNumberOf(fallback, ["fillQualityScore"])),
1450
+ 0,
1451
+ 100
1452
+ )
1453
+ };
1454
+ });
1455
+ }
1456
+
1457
+ function scopedMarket(scope, market, index) {
1458
+ const inheritedMarket = objectOf(scope, ["market", "marketInfo", "metadata", "instrument"], {});
1459
+ const perMarket = market && typeof market === "object" ? market : { symbol: String(market || `PERP-${index + 1}`) };
1460
+ return {
1461
+ ...scope,
1462
+ ...perMarket,
1463
+ market: {
1464
+ ...inheritedMarket,
1465
+ ...perMarket
1466
+ }
1467
+ };
1468
+ }
1469
+
1470
+ function summarizeAccounts(value) {
1471
+ const rows = rowsOf(value);
1472
+ const output = {};
1473
+ if (rows.length) output.activeAccounts = rows.length;
1474
+ const staleCount = rows.filter((row) => Boolean(valueOf(row, ["stale", "isStale", "catchupRequired"]))).length;
1475
+ if (staleCount) output.staleAccounts = staleCount;
1476
+ return output;
1477
+ }
1478
+
1479
+ function summarizeBitmap(bitmap) {
1480
+ if (!bitmap || typeof bitmap !== "object") return {};
1481
+ const output = {};
1482
+ const used = firstDefinedNumber(
1483
+ maybeNumberOf(bitmap, ["numUsed", "numUsedAccounts", "usedAccounts"]),
1484
+ Array.isArray(bitmap.usedIndices) ? bitmap.usedIndices.length : undefined,
1485
+ Array.isArray(bitmap.indices) ? bitmap.indices.length : undefined
1486
+ );
1487
+ const max = firstDefinedNumber(maybeNumberOf(bitmap, ["maxAccounts", "max_accounts", "capacity"]));
1488
+ if (used !== undefined) output.activeAccounts = used;
1489
+ if (max !== undefined) output.maxAccounts = max;
1490
+ return output;
1491
+ }
1492
+
1493
+ function cliOutputOf(entry) {
1494
+ const value = entry.output ?? entry.data ?? entry.result ?? entry.stdout ?? entry.stdoutText ?? entry.json ?? entry;
1495
+ if (typeof value !== "string") {
1496
+ assertReadOnlySnapshot(value, "cli.output");
1497
+ return value;
1498
+ }
1499
+ let parsed;
1500
+ try {
1501
+ parsed = parsePercolatorJson(value);
1502
+ } catch {
1503
+ return value;
1504
+ }
1505
+ assertReadOnlySnapshot(parsed, "cli.stdout");
1506
+ return parsed;
1507
+ }
1508
+
1509
+ function objectOf(source, aliases, fallback) {
1510
+ const value = valueOf(source, aliases);
1511
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
1512
+ return fallback;
1513
+ }
1514
+
1515
+ function arrayOf(source, aliases, fallback) {
1516
+ const value = valueOf(source, aliases);
1517
+ return Array.isArray(value) ? value : fallback;
1518
+ }
1519
+
1520
+ function stringOf(source, aliases, fallback = "") {
1521
+ const value = valueOf(source, aliases);
1522
+ if (value === undefined || value === null || value === "") return fallback;
1523
+ return String(value);
1524
+ }
1525
+
1526
+ function numberOf(source, aliases, fallback = 0) {
1527
+ const value = valueOf(source, aliases);
1528
+ if (value === undefined || value === null || value === "") return fallback;
1529
+ return number(value);
1530
+ }
1531
+
1532
+ function maybeNumberOf(source, aliases) {
1533
+ const value = valueOf(source, aliases);
1534
+ if (value === undefined || value === null || value === "") return undefined;
1535
+ return number(value);
1536
+ }
1537
+
1538
+ function priceNumberOf(source, usdAliases, rawAliases = []) {
1539
+ for (const alias of usdAliases) {
1540
+ const value = maybeNumberOf(source, [alias]);
1541
+ if (value === undefined) continue;
1542
+ if (normalizeKey(alias).includes("usd")) return value;
1543
+ const decimals = firstDefinedNumber(maybeNumberOf(source, ["decimals", "priceDecimals", "price_decimals"]));
1544
+ if (decimals !== undefined && Math.abs(value) >= 10 ** Math.min(decimals, 4)) {
1545
+ return value / 10 ** decimals;
1546
+ }
1547
+ if (decimals === undefined && Math.abs(value) > 1000000) continue;
1548
+ return value;
1549
+ }
1550
+
1551
+ const raw = firstDefinedNumber(maybeNumberOf(source, rawAliases));
1552
+ if (raw === undefined) return undefined;
1553
+
1554
+ const decimals = firstDefinedNumber(maybeNumberOf(source, ["decimals", "priceDecimals", "price_decimals"]));
1555
+ if (decimals === undefined || decimals < 0 || decimals > 18) return undefined;
1556
+ return raw / 10 ** decimals;
1557
+ }
1558
+
1559
+ function valueOf(source, aliases, fallback) {
1560
+ if (!source || typeof source !== "object") return fallback;
1561
+ const entries = Object.entries(source);
1562
+ for (const alias of aliases) {
1563
+ const wanted = normalizeKey(alias);
1564
+ const found = entries.find(([key]) => normalizeKey(key) === wanted);
1565
+ if (found && found[1] !== undefined && found[1] !== null) return found[1];
1566
+ }
1567
+ return fallback;
1568
+ }
1569
+
1570
+ function firstItem(value) {
1571
+ if (Array.isArray(value)) return value[0] || {};
1572
+ if (value?.items && Array.isArray(value.items)) return value.items[0] || {};
1573
+ if (value?.rows && Array.isArray(value.rows)) return value.rows[0] || {};
1574
+ if (value?.accounts && Array.isArray(value.accounts)) return value.accounts[0] || {};
1575
+ if (value && typeof value === "object") return value;
1576
+ return {};
1577
+ }
1578
+
1579
+ function rowsOf(value) {
1580
+ if (Array.isArray(value)) return value.filter((item) => item && typeof item === "object");
1581
+ if (Array.isArray(value?.items)) return rowsOf(value.items);
1582
+ if (Array.isArray(value?.rows)) return rowsOf(value.rows);
1583
+ if (Array.isArray(value?.accounts)) return rowsOf(value.accounts);
1584
+ return [];
1585
+ }
1586
+
1587
+ function firstNumber(...values) {
1588
+ for (const value of values) {
1589
+ if (value === undefined || value === null || value === "") continue;
1590
+ const next = number(value);
1591
+ if (Number.isFinite(next)) return next;
1592
+ }
1593
+ return 0;
1594
+ }
1595
+
1596
+ function firstDefinedNumber(...values) {
1597
+ for (const value of values) {
1598
+ if (value === undefined || value === null || value === "") continue;
1599
+ const next = number(value);
1600
+ if (Number.isFinite(next)) return next;
1601
+ }
1602
+ return undefined;
1603
+ }
1604
+
1605
+ function parseSymbol(symbol) {
1606
+ const [base = "PERP", quote = "USDC"] = String(symbol)
1607
+ .replace(/-?PERP$/i, "")
1608
+ .split(/[-/]/)
1609
+ .filter(Boolean);
1610
+ return { base: base.toUpperCase(), quote: quote.toUpperCase() };
1611
+ }
1612
+
1613
+ function extractJsonPayload(source) {
1614
+ const starts = [];
1615
+ for (let index = 0; index < source.length; index += 1) {
1616
+ if (source[index] === "{" || source[index] === "[") starts.push(index);
1617
+ }
1618
+ for (const start of starts) {
1619
+ const payload = balancedJsonPayload(source, start);
1620
+ if (!payload) continue;
1621
+ try {
1622
+ JSON.parse(payload);
1623
+ return payload;
1624
+ } catch {
1625
+ // Keep scanning; captured logs can contain bracketed prefixes before JSON.
1626
+ }
1627
+ }
1628
+ return "";
1629
+ }
1630
+
1631
+ function balancedJsonPayload(source, start) {
1632
+ const opener = source[start];
1633
+ const closer = opener === "{" ? "}" : "]";
1634
+ let depth = 0;
1635
+ let inString = false;
1636
+ let escaped = false;
1637
+
1638
+ for (let index = start; index < source.length; index += 1) {
1639
+ const char = source[index];
1640
+ if (escaped) {
1641
+ escaped = false;
1642
+ continue;
1643
+ }
1644
+ if (char === "\\") {
1645
+ escaped = true;
1646
+ continue;
1647
+ }
1648
+ if (char === "\"") {
1649
+ inString = !inString;
1650
+ continue;
1651
+ }
1652
+ if (inString) continue;
1653
+ if (char === opener) depth += 1;
1654
+ if (char === closer) depth -= 1;
1655
+ if (depth === 0) return source.slice(start, index + 1);
1656
+ }
1657
+
1658
+ return "";
1659
+ }
1660
+
1661
+ function normalizeKey(key) {
1662
+ return String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
1663
+ }
1664
+
1665
+ function number(value) {
1666
+ const next = Number(typeof value === "string" ? value.replace(/[$,%_\s,]/g, "") : value);
1667
+ return Number.isFinite(next) ? next : 0;
1668
+ }
1669
+
1670
+ function clamp(value, min, max) {
1671
+ return Math.min(max, Math.max(min, value));
1672
+ }