pandora-cli-skills 1.1.29 → 1.1.31

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.
Files changed (44) hide show
  1. package/SKILL.md +27 -1
  2. package/cli/lib/arbitrage_service.cjs +1 -12
  3. package/cli/lib/autopilot_service.cjs +1 -11
  4. package/cli/lib/cli_output_service.cjs +6 -0
  5. package/cli/lib/command_router.cjs +7 -1
  6. package/cli/lib/history_service.cjs +14 -26
  7. package/cli/lib/leaderboard_service.cjs +1 -6
  8. package/cli/lib/market_admin_service.cjs +45 -5
  9. package/cli/lib/mirror_command_service.cjs +149 -0
  10. package/cli/lib/mirror_econ_service.cjs +25 -41
  11. package/cli/lib/mirror_handlers/browse.cjs +46 -0
  12. package/cli/lib/mirror_handlers/close.cjs +31 -0
  13. package/cli/lib/mirror_handlers/deploy.cjs +53 -0
  14. package/cli/lib/mirror_handlers/go.cjs +276 -0
  15. package/cli/lib/mirror_handlers/hedge_calc.cjs +116 -0
  16. package/cli/lib/mirror_handlers/lp_explain.cjs +37 -0
  17. package/cli/lib/mirror_handlers/plan.cjs +46 -0
  18. package/cli/lib/mirror_handlers/simulate.cjs +37 -0
  19. package/cli/lib/mirror_handlers/status.cjs +165 -0
  20. package/cli/lib/mirror_handlers/sync.cjs +299 -0
  21. package/cli/lib/mirror_handlers/verify.cjs +73 -0
  22. package/cli/lib/mirror_service.cjs +1 -7
  23. package/cli/lib/mirror_sizing_service.cjs +1 -16
  24. package/cli/lib/mirror_sync_service.cjs +716 -386
  25. package/cli/lib/mirror_verify_service.cjs +11 -23
  26. package/cli/lib/pandora_deploy_service.cjs +6 -8
  27. package/cli/lib/parsers/autopilot_flags.cjs +257 -0
  28. package/cli/lib/parsers/mirror_deploy_flags.cjs +240 -0
  29. package/cli/lib/parsers/mirror_go_flags.cjs +342 -0
  30. package/cli/lib/parsers/mirror_hedge_calc_flags.cjs +187 -0
  31. package/cli/lib/parsers/mirror_plan_flags.cjs +151 -0
  32. package/cli/lib/parsers/mirror_sync_flags.cjs +445 -0
  33. package/cli/lib/parsers/trade_flags.cjs +180 -0
  34. package/cli/lib/parsers/watch_flags.cjs +212 -0
  35. package/cli/lib/polymarket_adapter.cjs +1 -6
  36. package/cli/lib/polymarket_trade_adapter.cjs +120 -35
  37. package/cli/lib/scan_command_service.cjs +11 -0
  38. package/cli/lib/shared/constants.cjs +23 -0
  39. package/cli/lib/shared/utils.cjs +63 -0
  40. package/cli/lib/similarity_service.cjs +1 -12
  41. package/cli/pandora.cjs +4341 -6823
  42. package/package.json +5 -2
  43. package/tests/smoke/consumer-json-smoke.cjs +217 -0
  44. package/tests/smoke/pack-install-smoke.cjs +302 -0
package/SKILL.md CHANGED
@@ -110,7 +110,7 @@ lp-explain --liquidity-usdc <n> [--source-yes-pct <0-100>] [--distribution-yes <
110
110
  hedge-calc [--reserve-yes-usdc <n> --reserve-no-usdc <n>] [--excess-yes-usdc <n>] [--excess-no-usdc <n>] [--polymarket-yes-pct <0-100>] [--hedge-ratio <n>] [--hedge-cost-bps <n>] [--volume-scenarios <csv>] [--pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug>] [--trust-deploy] [--manifest-file <path>]
111
111
  simulate --liquidity-usdc <n> [--source-yes-pct <0-100>] [--target-yes-pct <0-100>] [--distribution-yes <parts>] [--distribution-no <parts>] [--fee-tier 500|3000|10000] [--volume-scenarios <csv>] [--hedge-ratio <n>] [--hedge-cost-bps <n>] [--polymarket-yes-pct <0-100>]
112
112
  go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier 500|3000|10000] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--oracle <address>] [--factory <address>] [--sources <url...>] [--manifest-file <path>] [--trust-deploy] [--skip-gate] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--with-rules] [--include-similarity] [--min-close-lead-seconds <n>]
113
- sync run|once|start|stop|status --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> [--paper|--dry-run|--execute-live|--execute] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--trust-deploy] [--manifest-file <path>] [--skip-gate] [--daemon] [--stream|--no-stream] [--interval-ms <ms>] [--drift-trigger-bps <n>] [--hedge-trigger-usdc <n>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--depth-slippage-bps <n>] [--min-time-to-close-sec <n>] [--iterations <n>] [--state-file <path>] [--kill-switch-file <path>] [--chain-id <id>] [--rpc-url <url>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--webhook-url <url>] [--telegram-bot-token <token>] [--discord-webhook-url <url>]
113
+ sync run|once|start --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> [--paper|--dry-run|--execute-live|--execute] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--trust-deploy] [--manifest-file <path>] [--skip-gate] [--daemon] [--stream|--no-stream] [--interval-ms <ms>] [--drift-trigger-bps <n>] [--hedge-trigger-usdc <n>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--depth-slippage-bps <n>] [--min-time-to-close-sec <n>] [--iterations <n>] [--state-file <path>] [--kill-switch-file <path>] [--chain-id <id>] [--rpc-url <url>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--webhook-url <url>] [--telegram-bot-token <token>] [--telegram-chat-id <id>] [--discord-webhook-url <url>]
114
114
  status --state-file <path>|--strategy-hash <hash> [--with-live] [--pandora-market-address <address>|--market-address <address>] [--polymarket-market-id <id>|--polymarket-slug <slug>]
115
115
  close --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute
116
116
  ```
@@ -257,6 +257,7 @@ pandora --output json suggest --wallet <0x...> --risk medium --budget 50 --inclu
257
257
  - Polymarket trading collateral is Polygon USDC.e on the proxy wallet.
258
258
  - rebalance sizing is pool-aware and bounded by `--max-rebalance-usdc`.
259
259
  - endpoint resilience: Polymarket snapshots are cached under `~/.pandora/polymarket`; paper/read flows can reuse cache during outages, while live sync blocks cached sources.
260
+ - daemon verification note: `mirror sync start` launches a child process that executes `cli/pandora.cjs` and enters the same CLI `main()` handlers, so crash/error handling remains covered by the normal command dispatcher + structured error envelopes.
260
261
  - `mirror status`: local mirror state inspection with optional live market diagnostics (`--with-live`).
261
262
  - `--with-live` uses the same `POLYMARKET_*` env keys for optional position visibility (YES/NO balances, open orders count, estimated value) and adds `netDeltaApprox` / `pnlApprox`.
262
263
  - missing credentials or unavailable position endpoints do not hard-fail status; diagnostics are returned with null position fields.
@@ -346,6 +347,25 @@ Error envelope:
346
347
  ## Additional JSON response shapes
347
348
  - `doctor`:
348
349
  - `{ ok: true, command: "doctor", data: { schemaVersion, generatedAt, env, rpc, codeChecks, polymarket, summary } }`
350
+ - `history`:
351
+ - `{ ok: true, command: "history", data: { schemaVersion, generatedAt, indexerUrl, wallet, chainId, filters, pagination, pageInfo, summary, count, items[] } }`
352
+ - each `items[]` row includes `entryPriceUsdcPerToken`, `markPriceUsdcPerToken`, `currentValueUsdc`, `pnlUnrealizedApproxUsdc`, `pnlRealizedApproxUsdc`, `status`, and `diagnostics[]`.
353
+ - `export`:
354
+ - `{ ok: true, command: "export", data: { schemaVersion, generatedAt, format: "csv"|"json", wallet, chainId, count, filters, columns[], outPath, rows[], content } }`
355
+ - `content` is the deterministic serialized payload written to `outPath` when `--out` is provided.
356
+ - `arbitrage`:
357
+ - `{ ok: true, command: "arbitrage", data: { schemaVersion, generatedAt, indexerUrl, venues, filters, count, opportunities[], diagnostics[] } }`
358
+ - `autopilot`:
359
+ - `{ ok: true, command: "autopilot", data: { schemaVersion, generatedAt, strategyHash, mode, executeLive, stateFile, killSwitchFile, iterationsRequested, iterationsCompleted, stoppedReason?, parameters: { marketAddress, side, amountUsdc, triggerYesBelow?, triggerYesAbove?, intervalMs, cooldownMs, maxAmountUsdc?, maxOpenExposureUsdc, dailySpendCapUsdc, maxTradesPerDay }, state, actionCount, actions[], snapshots[], webhookReports[] } }`
360
+ - `mirror browse`:
361
+ - `{ ok: true, command: "mirror.browse", data: { schemaVersion, generatedAt, source, gammaApiError, filters, count, items[], diagnostics[] } }`
362
+ - each candidate row can include `existingMirror: { marketAddress, similarity } | null`.
363
+ - `mirror go`:
364
+ - `{ ok: true, command: "mirror.go", data: { schemaVersion, generatedAt, mode, plan, deploy, verify, sync, polymarketPreflight, suggestedSyncCommand, trustManifest, diagnostics[] } }`
365
+ - `plan` is the same payload shape as `mirror.plan`; `deploy` is the same payload shape as `mirror.deploy`; `sync` is null unless `--auto-sync` is used.
366
+ - close peers:
367
+ - `mirror close`: `{ ok: true, command: "mirror.close", data: { schemaVersion, generatedAt, mode, pandoraMarketAddress, polymarketMarketId?, polymarketSlug?, steps[], diagnostics[] } }`
368
+ - `mirror sync start|status|stop`: `{ ok: true, command: "mirror.sync.start|mirror.sync.status|mirror.sync.stop", data: { strategyHash, pid?, pidFile, logFile?, alive, status, metadata? } }`
349
369
  - `resolve`:
350
370
  - dry-run: `{ ok: true, command: "resolve", data: { schemaVersion, generatedAt, mode: "dry-run", txPlan } }`
351
371
  - execute: `{ ok: true, command: "resolve", data: { schemaVersion, generatedAt, mode: "execute", tx } }`
@@ -354,6 +374,12 @@ Error envelope:
354
374
  - `lp positions`: `{ ok: true, command: "lp", data: { schemaVersion, generatedAt, action: "positions", mode: "read", wallet, count, items[] } }`
355
375
  - `polymarket`:
356
376
  - `check|preflight|approve|trade` all return `{ ok: true, command, data }` with `schemaVersion` and `generatedAt`; execute paths include `result`/`tx` blocks.
377
+ - `leaderboard`:
378
+ - `{ ok: true, command: "leaderboard", data: { schemaVersion, generatedAt, indexerUrl, metric, limit, minTrades, count, items[], diagnostics[] } }`
379
+ - `analyze`:
380
+ - `{ ok: true, command: "analyze", data: { schemaVersion, generatedAt, indexerUrl, marketAddress, provider, model, market, quote, result } }`
381
+ - `suggest`:
382
+ - `{ ok: true, command: "suggest", data: { schemaVersion, generatedAt, wallet, risk, budget, count, items[], indexerUrl, includeVenues, historySummary, arbitrageCount } }`
357
383
 
358
384
  ## Pandora mainnet deployment reference
359
385
  - PredictionOracle (Factory): `0x259308E7d8557e4Ba192De1aB8Cf7e0E21896442`
@@ -5,21 +5,10 @@ const {
5
5
  questionSimilarityBreakdown,
6
6
  questionSimilarity,
7
7
  } = require('./similarity_service.cjs');
8
+ const { toNumber, round } = require('./shared/utils.cjs');
8
9
 
9
10
  const ARBITRAGE_SCHEMA_VERSION = '1.1.0';
10
11
 
11
- function toNumber(value) {
12
- const numeric = Number(value);
13
- if (!Number.isFinite(numeric)) return null;
14
- return numeric;
15
- }
16
-
17
- function round(value, decimals = 6) {
18
- if (!Number.isFinite(value)) return null;
19
- const factor = 10 ** decimals;
20
- return Math.round(value * factor) / factor;
21
- }
22
-
23
12
  function toUsdc(raw) {
24
13
  const numeric = toNumber(raw);
25
14
  if (numeric === null) return null;
@@ -9,17 +9,7 @@ const {
9
9
  pruneIdempotencyKeys,
10
10
  resetDailyCountersIfNeeded,
11
11
  } = require('./autopilot_state_store.cjs');
12
-
13
- function toNumber(value) {
14
- const numeric = Number(value);
15
- if (!Number.isFinite(numeric)) return null;
16
- return numeric;
17
- }
18
-
19
- function sleepMs(ms) {
20
- if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve();
21
- return new Promise((resolve) => setTimeout(resolve, ms));
22
- }
12
+ const { toNumber, sleepMs } = require('./shared/utils.cjs');
23
13
 
24
14
  function buildIdempotencyKey(options, quote, nowMs) {
25
15
  const yesPct = toNumber(quote && quote.odds && quote.odds.yesPct);
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Create CLI output helpers for success/error envelopes and table output.
3
+ * @param {{defaultSchemaVersion?: string, CliError: Function}} [options]
4
+ * @returns {{emitFailure: (outputMode: 'table'|'json', error: any) => void, emitSuccess: (outputMode: 'table'|'json', command: string, data: any, tableRenderer?: Function) => void}}
5
+ */
1
6
  function createCliOutputService(options = {}) {
2
7
  const defaultSchemaVersion =
3
8
  typeof options.defaultSchemaVersion === 'string' && options.defaultSchemaVersion.trim()
@@ -120,6 +125,7 @@ function createCliOutputService(options = {}) {
120
125
  };
121
126
  }
122
127
 
128
+ /** Public CLI output service factory export. */
123
129
  module.exports = {
124
130
  createCliOutputService,
125
131
  };
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Build the top-level command dispatcher for `pandora`.
3
+ * The returned function routes parsed command/args/context to subcommand handlers.
4
+ * @param {object} deps
5
+ * @returns {(command: string|undefined, args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}
6
+ */
1
7
  function createCommandRouter(deps = {}) {
2
8
  const {
3
9
  CliError,
@@ -156,7 +162,7 @@ function createCommandRouter(deps = {}) {
156
162
  };
157
163
  }
158
164
 
165
+ /** Public command-router factory export. */
159
166
  module.exports = {
160
167
  createCommandRouter,
161
168
  };
162
-
@@ -1,29 +1,17 @@
1
1
  const { createIndexerClient } = require('./indexer_client.cjs');
2
+ const { round, toOptionalNumber } = require('./shared/utils.cjs');
2
3
 
3
4
  const USDC_DECIMALS = 6;
4
5
  const HISTORY_SCHEMA_VERSION = '1.0.0';
5
6
 
6
- function toNumber(value) {
7
- if (value === null || value === undefined || value === '') return null;
8
- const numeric = Number(value);
9
- if (!Number.isFinite(numeric)) return null;
10
- return numeric;
11
- }
12
-
13
- function round(value, decimals = 6) {
14
- if (!Number.isFinite(value)) return null;
15
- const factor = 10 ** decimals;
16
- return Math.round(value * factor) / factor;
17
- }
18
-
19
7
  function toUsdc(raw) {
20
- const numeric = toNumber(raw);
8
+ const numeric = toOptionalNumber(raw);
21
9
  if (numeric === null) return null;
22
10
  return round(numeric / 10 ** USDC_DECIMALS, 6);
23
11
  }
24
12
 
25
13
  function normalizeProbabilityFromYesChance(rawYesChance) {
26
- const raw = toNumber(rawYesChance);
14
+ const raw = toOptionalNumber(rawYesChance);
27
15
  if (raw === null) return null;
28
16
  if (raw >= 0 && raw <= 1) return raw;
29
17
  if (raw > 1 && raw <= 100) return raw / 100;
@@ -34,8 +22,8 @@ function computeYesProbabilityFromMarket(market) {
34
22
  const yesChanceProb = normalizeProbabilityFromYesChance(market && market.yesChance);
35
23
  if (yesChanceProb !== null && yesChanceProb >= 0 && yesChanceProb <= 1) return yesChanceProb;
36
24
 
37
- const reserveYes = toNumber(market && market.reserveYes);
38
- const reserveNo = toNumber(market && market.reserveNo);
25
+ const reserveYes = toOptionalNumber(market && market.reserveYes);
26
+ const reserveNo = toOptionalNumber(market && market.reserveNo);
39
27
  if (reserveYes === null || reserveNo === null) return null;
40
28
  const total = reserveYes + reserveNo;
41
29
  if (!Number.isFinite(total) || total <= 0) return null;
@@ -117,21 +105,21 @@ function buildSummary(items) {
117
105
  };
118
106
 
119
107
  for (const item of items) {
120
- const collateral = toNumber(item.collateralAmountUsdc) || 0;
108
+ const collateral = toOptionalNumber(item.collateralAmountUsdc) || 0;
121
109
  summary.grossVolumeUsdc += collateral;
122
110
 
123
111
  if (item.status === 'open') {
124
112
  summary.openCount += 1;
125
- summary.unrealizedPnlApproxUsdc += toNumber(item.pnlUnrealizedApproxUsdc) || 0;
113
+ summary.unrealizedPnlApproxUsdc += toOptionalNumber(item.pnlUnrealizedApproxUsdc) || 0;
126
114
  } else if (item.status === 'won') {
127
115
  summary.wonCount += 1;
128
- summary.realizedPnlApproxUsdc += toNumber(item.pnlRealizedApproxUsdc) || 0;
116
+ summary.realizedPnlApproxUsdc += toOptionalNumber(item.pnlRealizedApproxUsdc) || 0;
129
117
  } else if (item.status === 'lost') {
130
118
  summary.lostCount += 1;
131
- summary.realizedPnlApproxUsdc += toNumber(item.pnlRealizedApproxUsdc) || 0;
119
+ summary.realizedPnlApproxUsdc += toOptionalNumber(item.pnlRealizedApproxUsdc) || 0;
132
120
  } else {
133
121
  summary.closedOtherCount += 1;
134
- summary.realizedPnlApproxUsdc += toNumber(item.pnlRealizedApproxUsdc) || 0;
122
+ summary.realizedPnlApproxUsdc += toOptionalNumber(item.pnlRealizedApproxUsdc) || 0;
135
123
  }
136
124
  }
137
125
 
@@ -146,15 +134,15 @@ function sortHistoryItems(items, orderBy, orderDirection) {
146
134
  const dir = String(orderDirection || 'desc').toLowerCase() === 'asc' ? 1 : -1;
147
135
  const pick = (item) => {
148
136
  if (orderBy === 'pnl') {
149
- return toNumber(item.pnlRealizedApproxUsdc) ?? toNumber(item.pnlUnrealizedApproxUsdc) ?? -Infinity;
137
+ return toOptionalNumber(item.pnlRealizedApproxUsdc) ?? toOptionalNumber(item.pnlUnrealizedApproxUsdc) ?? -Infinity;
150
138
  }
151
139
  if (orderBy === 'entry-price') {
152
- return toNumber(item.entryPriceUsdcPerToken) ?? -Infinity;
140
+ return toOptionalNumber(item.entryPriceUsdcPerToken) ?? -Infinity;
153
141
  }
154
142
  if (orderBy === 'mark-price') {
155
- return toNumber(item.markPriceUsdcPerToken) ?? -Infinity;
143
+ return toOptionalNumber(item.markPriceUsdcPerToken) ?? -Infinity;
156
144
  }
157
- return toNumber(item.timestamp) ?? 0;
145
+ return toOptionalNumber(item.timestamp) ?? 0;
158
146
  };
159
147
 
160
148
  items.sort((a, b) => {
@@ -1,13 +1,8 @@
1
1
  const { createIndexerClient } = require('./indexer_client.cjs');
2
+ const { toNumber } = require('./shared/utils.cjs');
2
3
 
3
4
  const LEADERBOARD_SCHEMA_VERSION = '1.0.0';
4
5
 
5
- function toNumber(value) {
6
- const numeric = Number(value);
7
- if (!Number.isFinite(numeric)) return null;
8
- return numeric;
9
- }
10
-
11
6
  function toCount(value) {
12
7
  const numeric = toNumber(value);
13
8
  if (numeric === null || numeric < 0) return 0;
@@ -1,8 +1,4 @@
1
- const DEFAULT_INDEXER_URL = 'https://pandoraindexer.up.railway.app/';
2
- const DEFAULT_RPC_BY_CHAIN_ID = {
3
- 1: 'https://ethereum.publicnode.com',
4
- 146: 'https://rpc.soniclabs.com',
5
- };
1
+ const { DEFAULT_INDEXER_URL, DEFAULT_RPC_BY_CHAIN_ID } = require('./shared/constants.cjs');
6
2
 
7
3
  const ERC20_ABI = [
8
4
  {
@@ -180,6 +176,12 @@ function normalizeTimeoutMs(value) {
180
176
  return Math.trunc(numeric);
181
177
  }
182
178
 
179
+ /**
180
+ * Convert numeric-like input into a finite number.
181
+ * Used by LP/resolve input normalization paths.
182
+ * @param {*} value
183
+ * @returns {number|null}
184
+ */
183
185
  function toFiniteNumber(value) {
184
186
  const numeric = Number(value);
185
187
  if (!Number.isFinite(numeric)) return null;
@@ -313,6 +315,14 @@ async function readDecimals(publicClient, address, fallback = 18) {
313
315
  }
314
316
  }
315
317
 
318
+ /**
319
+ * Read optional `calcRemoveLiquidity` preview outputs.
320
+ * Returned values are raw on-chain integers as decimal strings.
321
+ * @param {object} publicClient
322
+ * @param {string} marketAddress
323
+ * @param {bigint} sharesRaw
324
+ * @returns {Promise<{collateralOutRaw: string|null, yesOutRaw: string|null, noOutRaw: string|null}|null>}
325
+ */
316
326
  async function readCalcRemoveLiquidity(publicClient, marketAddress, sharesRaw) {
317
327
  for (const abi of CALC_REMOVE_LIQUIDITY_ABI_CANDIDATES) {
318
328
  try {
@@ -395,6 +405,11 @@ async function discoverLiquidityMarkets(indexerUrl, wallet, chainId, timeoutMs)
395
405
  return Array.from(new Set(addresses));
396
406
  }
397
407
 
408
+ /**
409
+ * Resolve a market outcome by calling `resolveMarket(bool)`.
410
+ * @param {object} [options]
411
+ * @returns {Promise<object>}
412
+ */
398
413
  async function runResolve(options = {}) {
399
414
  const schemaVersion = '1.0.0';
400
415
  const generatedAt = new Date().toISOString();
@@ -468,6 +483,13 @@ async function runResolve(options = {}) {
468
483
  }
469
484
  }
470
485
 
486
+ /**
487
+ * Read LP balances and remove-liquidity previews for a wallet.
488
+ * LP/share values are raw and human-readable decimal strings in the payload.
489
+ * `collateralOutUsdc` is decimal USDC (6 decimals).
490
+ * @param {object} [options]
491
+ * @returns {Promise<object>}
492
+ */
471
493
  async function runLpPositions(options = {}) {
472
494
  const schemaVersion = '1.0.0';
473
495
  const generatedAt = new Date().toISOString();
@@ -570,6 +592,12 @@ async function runLpPositions(options = {}) {
570
592
  };
571
593
  }
572
594
 
595
+ /**
596
+ * Build or execute `addLiquidity`.
597
+ * `amountUsdc` is decimal USDC input; `collateralAmountRaw` is 6-decimal raw units.
598
+ * @param {object} [options]
599
+ * @returns {Promise<object>}
600
+ */
573
601
  async function runLpAdd(options = {}) {
574
602
  const schemaVersion = '1.0.0';
575
603
  const generatedAt = new Date().toISOString();
@@ -719,6 +747,12 @@ async function runLpAdd(options = {}) {
719
747
  }
720
748
  }
721
749
 
750
+ /**
751
+ * Build or execute `removeLiquidity`.
752
+ * `lpTokens` is decimal LP token amount; `sharesToBurnRaw` is on-chain raw units.
753
+ * @param {object} [options]
754
+ * @returns {Promise<object>}
755
+ */
722
756
  async function runLpRemove(options = {}) {
723
757
  const schemaVersion = '1.0.0';
724
758
  const generatedAt = new Date().toISOString();
@@ -825,6 +859,11 @@ async function runLpRemove(options = {}) {
825
859
  }
826
860
  }
827
861
 
862
+ /**
863
+ * Dispatch LP admin action (`positions`, `add`, `remove`).
864
+ * @param {object} [options]
865
+ * @returns {Promise<object>}
866
+ */
828
867
  async function runLp(options = {}) {
829
868
  if (options.action === 'positions') {
830
869
  return runLpPositions(options);
@@ -838,6 +877,7 @@ async function runLp(options = {}) {
838
877
  throw createServiceError('INVALID_ARGS', 'lp requires action add|remove|positions.');
839
878
  }
840
879
 
880
+ /** Public market admin API consumed by CLI `resolve` and `lp` commands. */
841
881
  module.exports = {
842
882
  runResolve,
843
883
  runLp,
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Canonical usage string for `mirror go`.
3
+ * Exported for CLI help rendering and integration tests.
4
+ * @type {string}
5
+ */
6
+ const MIRROR_GO_USAGE =
7
+ 'pandora [--output table|json] mirror go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier 500|3000|10000] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--paper|--dry-run|--execute-live|--execute] [--auto-sync] [--sync-once] [--sync-interval-ms <ms>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--oracle <address>] [--factory <address>] [--sources <url...>] [--manifest-file <path>] [--trust-deploy] [--skip-gate] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--with-rules] [--include-similarity] [--min-close-lead-seconds <n>]';
8
+
9
+ /**
10
+ * Canonical usage string for `mirror sync`.
11
+ * Exported for CLI help rendering and integration tests.
12
+ * @type {string}
13
+ */
14
+ const MIRROR_SYNC_USAGE =
15
+ 'pandora [--output table|json] mirror sync run|once|start --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> [--paper|--dry-run|--execute-live|--execute] [--private-key <hex>] [--funder <address>] [--usdc <address>] [--trust-deploy] [--manifest-file <path>] [--skip-gate] [--daemon] [--stream|--no-stream] [--interval-ms <ms>] [--drift-trigger-bps <n>] [--hedge-trigger-usdc <n>] [--hedge-ratio <n>] [--no-hedge] [--max-rebalance-usdc <n>] [--max-hedge-usdc <n>] [--max-open-exposure-usdc <n>] [--max-trades-per-day <n>] [--cooldown-ms <ms>] [--depth-slippage-bps <n>] [--min-time-to-close-sec <n>] [--iterations <n>] [--state-file <path>] [--kill-switch-file <path>] [--chain-id <id>] [--rpc-url <url>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--webhook-url <url>] [--telegram-bot-token <token>] [--telegram-chat-id <id>] [--discord-webhook-url <url>]';
16
+
17
+ const INVALID_SUBCOMMAND_MESSAGE =
18
+ 'mirror requires subcommand: browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close';
19
+
20
+ /**
21
+ * Build the `mirror` subcommand dispatcher with lazy-loaded action handlers.
22
+ * @param {object} deps
23
+ * @returns {(args: string[], context: {outputMode: 'table'|'json'}) => Promise<any>}
24
+ */
25
+ function createRunMirrorCommand(deps) {
26
+ const {
27
+ CliError,
28
+ emitSuccess,
29
+ commandHelpPayload,
30
+ parseIndexerSharedFlags,
31
+ } = deps;
32
+
33
+ const handlerLoaders = {
34
+ browse: () => require('./mirror_handlers/browse.cjs'),
35
+ plan: () => require('./mirror_handlers/plan.cjs'),
36
+ deploy: () => require('./mirror_handlers/deploy.cjs'),
37
+ verify: () => require('./mirror_handlers/verify.cjs'),
38
+ 'lp-explain': () => require('./mirror_handlers/lp_explain.cjs'),
39
+ 'hedge-calc': () => require('./mirror_handlers/hedge_calc.cjs'),
40
+ simulate: () => require('./mirror_handlers/simulate.cjs'),
41
+ go: () => require('./mirror_handlers/go.cjs'),
42
+ sync: () => require('./mirror_handlers/sync.cjs'),
43
+ status: () => require('./mirror_handlers/status.cjs'),
44
+ close: () => require('./mirror_handlers/close.cjs'),
45
+ };
46
+ const handlerCache = new Map();
47
+
48
+ function getHandler(action) {
49
+ if (handlerCache.has(action)) {
50
+ return handlerCache.get(action);
51
+ }
52
+
53
+ const load = handlerLoaders[action];
54
+ if (!load) {
55
+ return null;
56
+ }
57
+
58
+ const loaded = load();
59
+ const handler =
60
+ typeof loaded === 'function'
61
+ ? loaded
62
+ : loaded && typeof loaded.handle === 'function'
63
+ ? loaded.handle
64
+ : null;
65
+
66
+ if (!handler) {
67
+ throw new Error(`Invalid mirror handler module for action: ${action}`);
68
+ }
69
+
70
+ handlerCache.set(action, handler);
71
+ return handler;
72
+ }
73
+
74
+ return async function runMirrorCommand(args, context) {
75
+ const action = args[0];
76
+ const actionArgs = args.slice(1);
77
+
78
+ if (!action || action === '--help' || action === '-h') {
79
+ if (context.outputMode === 'json') {
80
+ emitSuccess(
81
+ context.outputMode,
82
+ 'mirror.help',
83
+ commandHelpPayload(
84
+ 'pandora [--output table|json] mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close ...',
85
+ ),
86
+ );
87
+ } else {
88
+ console.log(
89
+ 'Usage: pandora [--output table|json] mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close ...',
90
+ );
91
+ console.log('');
92
+ console.log('Subcommands:');
93
+ console.log(
94
+ ' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
95
+ );
96
+ console.log(
97
+ ' plan --source polymarket --polymarket-market-id <id>|--polymarket-slug <slug> [--chain-id <id>] [--target-slippage-bps <n>] [--turnover-target <n>] [--depth-slippage-bps <n>] [--safety-multiplier <n>] [--min-liquidity-usdc <n>] [--max-liquidity-usdc <n>] [--with-rules] [--include-similarity] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
98
+ );
99
+ console.log(
100
+ ' deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier 500|3000|10000] [--max-imbalance <n>] [--arbiter <address>] [--category <n>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--oracle <address>] [--factory <address>] [--usdc <address>] [--distribution-yes <parts>] [--distribution-no <parts>] [--sources <url...>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>] [--min-close-lead-seconds <n>]',
101
+ );
102
+ console.log(
103
+ ' verify --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> [--trust-deploy] [--manifest-file <path>] [--include-similarity] [--with-rules] [--allow-rule-mismatch] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
104
+ );
105
+ console.log(
106
+ ' lp-explain --liquidity-usdc <n> [--source-yes-pct <0-100>] [--distribution-yes <parts>] [--distribution-no <parts>]',
107
+ );
108
+ console.log(
109
+ ' hedge-calc [--reserve-yes-usdc <n> --reserve-no-usdc <n>] [--excess-yes-usdc <n>] [--excess-no-usdc <n>] [--polymarket-yes-pct <0-100>] [--hedge-ratio <n>] [--hedge-cost-bps <n>] [--volume-scenarios <csv>] [--pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug>]',
110
+ );
111
+ console.log(
112
+ ' simulate --liquidity-usdc <n> [--source-yes-pct <0-100>] [--target-yes-pct <0-100>] [--distribution-yes <parts>] [--distribution-no <parts>] [--fee-tier 500|3000|10000] [--volume-scenarios <csv>] [--hedge-ratio <n>] [--polymarket-yes-pct <0-100>]',
113
+ );
114
+ console.log(
115
+ ` go ${MIRROR_GO_USAGE.replace('pandora [--output table|json] mirror go ', '')}`,
116
+ );
117
+ console.log(
118
+ ` sync ${MIRROR_SYNC_USAGE.replace('pandora [--output table|json] mirror sync ', '')}`,
119
+ );
120
+ console.log(' stop|status selector: --pid-file <path>|--strategy-hash <hash>');
121
+ console.log(' status --state-file <path>|--strategy-hash <hash> [--with-live] [--trust-deploy]');
122
+ console.log(' close --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute');
123
+ }
124
+ return;
125
+ }
126
+
127
+ const handler = getHandler(action);
128
+ if (!handler) {
129
+ throw new CliError('INVALID_ARGS', INVALID_SUBCOMMAND_MESSAGE);
130
+ }
131
+ const shared = parseIndexerSharedFlags(actionArgs);
132
+
133
+ return handler({
134
+ actionArgs: shared.rest,
135
+ shared,
136
+ context,
137
+ deps,
138
+ mirrorGoUsage: MIRROR_GO_USAGE,
139
+ mirrorSyncUsage: MIRROR_SYNC_USAGE,
140
+ });
141
+ };
142
+ }
143
+
144
+ /** Public mirror command service exports. */
145
+ module.exports = {
146
+ MIRROR_GO_USAGE,
147
+ MIRROR_SYNC_USAGE,
148
+ createRunMirrorCommand,
149
+ };
@@ -1,4 +1,5 @@
1
1
  const { computeDistributionHint, normalizeProbability } = require('./mirror_sizing_service.cjs');
2
+ const { round, clamp, toOptionalNumber } = require('./shared/utils.cjs');
2
3
 
3
4
  const MIRROR_LP_EXPLAIN_SCHEMA_VERSION = '1.0.0';
4
5
  const MIRROR_HEDGE_CALC_SCHEMA_VERSION = '1.0.0';
@@ -6,25 +7,8 @@ const MIRROR_SIMULATE_SCHEMA_VERSION = '1.0.0';
6
7
  const USDC_DECIMALS = 6;
7
8
  const USDC_SCALE = 10 ** USDC_DECIMALS;
8
9
 
9
- function toNumber(value) {
10
- if (value === null || value === undefined || value === '') return null;
11
- const numeric = Number(value);
12
- if (!Number.isFinite(numeric)) return null;
13
- return numeric;
14
- }
15
-
16
- function round(value, decimals = 6) {
17
- if (!Number.isFinite(value)) return null;
18
- const factor = 10 ** decimals;
19
- return Math.round(value * factor) / factor;
20
- }
21
-
22
- function clamp(value, min, max) {
23
- return Math.max(min, Math.min(max, value));
24
- }
25
-
26
10
  function toRawUsdc(usdc) {
27
- const numeric = toNumber(usdc);
11
+ const numeric = toOptionalNumber(usdc);
28
12
  if (numeric === null) return null;
29
13
  return BigInt(Math.round(numeric * USDC_SCALE));
30
14
  }
@@ -35,7 +19,7 @@ function rawUsdcToNumber(rawValue) {
35
19
  }
36
20
 
37
21
  function toPercent(value) {
38
- const numeric = toNumber(value);
22
+ const numeric = toOptionalNumber(value);
39
23
  if (numeric === null) return null;
40
24
  if (numeric >= 0 && numeric <= 100) return round(numeric, 6);
41
25
  return null;
@@ -51,8 +35,8 @@ function resolveDistribution(options = {}) {
51
35
  const diagnostics = [];
52
36
  const sourceYesPct = toPercent(options.sourceYesPct);
53
37
 
54
- const explicitYes = toNumber(options.distributionYes);
55
- const explicitNo = toNumber(options.distributionNo);
38
+ const explicitYes = toOptionalNumber(options.distributionYes);
39
+ const explicitNo = toOptionalNumber(options.distributionNo);
56
40
  if (explicitYes !== null || explicitNo !== null) {
57
41
  if (explicitYes === null || explicitNo === null) {
58
42
  throw new Error('distributionYes and distributionNo must both be provided together.');
@@ -94,8 +78,8 @@ function resolveDistribution(options = {}) {
94
78
  }
95
79
 
96
80
  function deriveYesPctFromReserves(reserveYesUsdc, reserveNoUsdc) {
97
- const yesReserve = toNumber(reserveYesUsdc);
98
- const noReserve = toNumber(reserveNoUsdc);
81
+ const yesReserve = toOptionalNumber(reserveYesUsdc);
82
+ const noReserve = toOptionalNumber(reserveNoUsdc);
99
83
  if (yesReserve === null || noReserve === null) return null;
100
84
  const total = yesReserve + noReserve;
101
85
  if (!Number.isFinite(total) || total <= 0) return null;
@@ -108,8 +92,8 @@ function computeCompleteSetAllocation(options = {}) {
108
92
  throw new Error('liquidityUsdc must be a positive number.');
109
93
  }
110
94
 
111
- const distributionYes = toNumber(options.distributionYes);
112
- const distributionNo = toNumber(options.distributionNo);
95
+ const distributionYes = toOptionalNumber(options.distributionYes);
96
+ const distributionNo = toOptionalNumber(options.distributionNo);
113
97
  if (!Number.isInteger(distributionYes) || !Number.isInteger(distributionNo)) {
114
98
  throw new Error('distributionYes and distributionNo must be integers.');
115
99
  }
@@ -163,9 +147,9 @@ function computeCompleteSetAllocation(options = {}) {
163
147
  }
164
148
 
165
149
  function simulateDirectionalSwap(options = {}) {
166
- const reserveYes = Math.max(0, toNumber(options.reserveYesUsdc) || 0);
167
- const reserveNo = Math.max(0, toNumber(options.reserveNoUsdc) || 0);
168
- const volumeUsdc = Math.max(0, toNumber(options.volumeUsdc) || 0);
150
+ const reserveYes = Math.max(0, toOptionalNumber(options.reserveYesUsdc) || 0);
151
+ const reserveNo = Math.max(0, toOptionalNumber(options.reserveNoUsdc) || 0);
152
+ const volumeUsdc = Math.max(0, toOptionalNumber(options.volumeUsdc) || 0);
169
153
  const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
170
154
  const feeRate = clamp(feeTier / 1_000_000, 0, 0.1);
171
155
  const side = String(options.side || 'none').toLowerCase();
@@ -224,8 +208,8 @@ function simulateDirectionalSwap(options = {}) {
224
208
 
225
209
  function solveVolumeForTargetYesPct(options = {}) {
226
210
  const targetYesPct = toPercent(options.targetYesPct);
227
- const reserveYesUsdc = toNumber(options.reserveYesUsdc);
228
- const reserveNoUsdc = toNumber(options.reserveNoUsdc);
211
+ const reserveYesUsdc = toOptionalNumber(options.reserveYesUsdc);
212
+ const reserveNoUsdc = toOptionalNumber(options.reserveNoUsdc);
229
213
  const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
230
214
 
231
215
  if (targetYesPct === null || reserveYesUsdc === null || reserveNoUsdc === null) {
@@ -276,7 +260,7 @@ function solveVolumeForTargetYesPct(options = {}) {
276
260
 
277
261
  function normalizeVolumeScenarios(value, liquidityUsdc) {
278
262
  const fallback = () => {
279
- const liq = Math.max(1, toNumber(liquidityUsdc) || 0);
263
+ const liq = Math.max(1, toOptionalNumber(liquidityUsdc) || 0);
280
264
  return [
281
265
  round(liq * 0.25, 6),
282
266
  round(liq * 0.5, 6),
@@ -286,7 +270,7 @@ function normalizeVolumeScenarios(value, liquidityUsdc) {
286
270
  };
287
271
 
288
272
  if (Array.isArray(value)) {
289
- const parsed = value.map((item) => toNumber(item)).filter((item) => Number.isFinite(item) && item >= 0).map((item) => round(item, 6));
273
+ const parsed = value.map((item) => toOptionalNumber(item)).filter((item) => Number.isFinite(item) && item >= 0).map((item) => round(item, 6));
290
274
  return parsed.length ? parsed : fallback();
291
275
  }
292
276
 
@@ -294,7 +278,7 @@ function normalizeVolumeScenarios(value, liquidityUsdc) {
294
278
  if (raw) {
295
279
  const parsed = raw
296
280
  .split(',')
297
- .map((entry) => toNumber(entry.trim()))
281
+ .map((entry) => toOptionalNumber(entry.trim()))
298
282
  .filter((entry) => Number.isFinite(entry) && entry >= 0)
299
283
  .map((entry) => round(entry, 6));
300
284
  return parsed.length ? parsed : fallback();
@@ -304,13 +288,13 @@ function normalizeVolumeScenarios(value, liquidityUsdc) {
304
288
  }
305
289
 
306
290
  function computeHedgeMetrics(options = {}) {
307
- const reserveYesUsdc = toNumber(options.reserveYesUsdc) || 0;
308
- const reserveNoUsdc = toNumber(options.reserveNoUsdc) || 0;
309
- const excessYesUsdc = toNumber(options.excessYesUsdc) || 0;
310
- const excessNoUsdc = toNumber(options.excessNoUsdc) || 0;
311
- const hedgeRatio = clamp(toNumber(options.hedgeRatio) || 1, 0, 5);
291
+ const reserveYesUsdc = toOptionalNumber(options.reserveYesUsdc) || 0;
292
+ const reserveNoUsdc = toOptionalNumber(options.reserveNoUsdc) || 0;
293
+ const excessYesUsdc = toOptionalNumber(options.excessYesUsdc) || 0;
294
+ const excessNoUsdc = toOptionalNumber(options.excessNoUsdc) || 0;
295
+ const hedgeRatio = clamp(toOptionalNumber(options.hedgeRatio) || 1, 0, 5);
312
296
  const polymarketYesPct = toPercent(options.polymarketYesPct);
313
- const hedgeCostBps = Math.max(0, toNumber(options.hedgeCostBps) || 35);
297
+ const hedgeCostBps = Math.max(0, toOptionalNumber(options.hedgeCostBps) || 35);
314
298
  const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
315
299
  const feeRate = clamp(feeTier / 1_000_000, 0, 0.1);
316
300
 
@@ -453,8 +437,8 @@ function buildMirrorSimulate(options = {}) {
453
437
  : 'none';
454
438
 
455
439
  const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
456
- const hedgeRatio = clamp(toNumber(options.hedgeRatio) || 1, 0, 5);
457
- const hedgeCostBps = Math.max(0, toNumber(options.hedgeCostBps) || 35);
440
+ const hedgeRatio = clamp(toOptionalNumber(options.hedgeRatio) || 1, 0, 5);
441
+ const hedgeCostBps = Math.max(0, toOptionalNumber(options.hedgeCostBps) || 35);
458
442
  const polymarketYesPct = toPercent(
459
443
  options.polymarketYesPct !== undefined ? options.polymarketYesPct : (targetYesPct !== null ? targetYesPct : 50),
460
444
  );