pandora-cli-skills 1.1.41 → 1.1.42

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 (47) hide show
  1. package/README.md +27 -1
  2. package/README_FOR_SHARING.md +12 -0
  3. package/SKILL.md +17 -3
  4. package/cli/lib/arb_command_service.cjs +336 -0
  5. package/cli/lib/command_router.cjs +12 -0
  6. package/cli/lib/connectors/pandora_amm_connector.cjs +200 -0
  7. package/cli/lib/connectors/polymarket_connector.cjs +154 -0
  8. package/cli/lib/error_recovery_service.cjs +52 -0
  9. package/cli/lib/export_service.cjs +20 -0
  10. package/cli/lib/fork_preview_service.cjs +361 -0
  11. package/cli/lib/history_service.cjs +2 -0
  12. package/cli/lib/lifecycle_command_service.cjs +294 -0
  13. package/cli/lib/lp_command_service.cjs +7 -0
  14. package/cli/lib/mcp_tool_registry.cjs +27 -0
  15. package/cli/lib/mirror_handlers/deploy.cjs +7 -0
  16. package/cli/lib/mirror_handlers/go.cjs +13 -0
  17. package/cli/lib/mirror_handlers/sync.cjs +7 -0
  18. package/cli/lib/mirror_sync_service.cjs +3 -1
  19. package/cli/lib/odds_history_service.cjs +336 -0
  20. package/cli/lib/parsers/lifecycle_flags.cjs +93 -0
  21. package/cli/lib/parsers/odds_flags.cjs +154 -0
  22. package/cli/lib/parsers/risk_flags.cjs +82 -0
  23. package/cli/lib/parsers/sports_flags.cjs +19 -1
  24. package/cli/lib/polymarket_command_service.cjs +29 -3
  25. package/cli/lib/resolve_command_service.cjs +6 -0
  26. package/cli/lib/risk_command_service.cjs +111 -0
  27. package/cli/lib/risk_guard_service.cjs +286 -0
  28. package/cli/lib/risk_state_store.cjs +282 -0
  29. package/cli/lib/schema_command_service.cjs +27 -0
  30. package/cli/lib/sports_command_service.cjs +394 -141
  31. package/cli/lib/sports_creation_service.cjs +39 -3
  32. package/cli/lib/sports_model_input_service.cjs +98 -0
  33. package/cli/lib/trade_command_service.cjs +17 -0
  34. package/cli/lib/venue_connector_factory.cjs +87 -0
  35. package/cli/pandora.cjs +420 -1
  36. package/package.json +2 -2
  37. package/tests/cli/cli.integration.test.cjs +311 -0
  38. package/tests/cli/mcp.integration.test.cjs +111 -2
  39. package/tests/cli/sports.integration.test.cjs +139 -4
  40. package/tests/unit/export_service.test.cjs +87 -0
  41. package/tests/unit/fork_preview_service.test.cjs +238 -0
  42. package/tests/unit/new-features.test.cjs +99 -0
  43. package/tests/unit/odds_history_service.test.cjs +132 -0
  44. package/tests/unit/risk_guard_service.test.cjs +175 -0
  45. package/tests/unit/risk_state_store.test.cjs +166 -0
  46. package/tests/unit/sports_creation.test.cjs +47 -0
  47. package/tests/unit/venue_connector_factory.test.cjs +67 -0
package/README.md CHANGED
@@ -27,6 +27,7 @@ npx pandora-cli-skills@latest --help
27
27
  - `--fork`, `--fork-rpc-url`, `--fork-chain-id`.
28
28
  - Event-driven streaming:
29
29
  - `pandora stream prices|events` emits NDJSON lines on stdout.
30
+ - MCP tool surface includes risk/lifecycle/odds command families in addition to core read/trade flows.
30
31
 
31
32
  ## Quickstart
32
33
 
@@ -63,6 +64,27 @@ pandora --output json sports create plan --event-id <event-id> --selection home
63
64
  pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>
64
65
  ```
65
66
 
67
+ ### Lifecycle Quickstart
68
+
69
+ ```bash
70
+ # lifecycle config must be JSON object (not YAML in current release)
71
+ pandora --output json lifecycle start --config ./configs/lifecycle.json
72
+ pandora --output json lifecycle status --id <lifecycle-id>
73
+ pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
74
+ ```
75
+
76
+ ## Risk Controls
77
+
78
+ - Inspect/engage panic lock:
79
+ - `pandora --output json risk show`
80
+ - `pandora --output json risk panic --reason "incident"`
81
+ - `pandora --output json risk panic --clear`
82
+ - Current guardrail semantics:
83
+ - `max_position_usd` guards per-operation notional.
84
+ - `max_daily_loss_usd` currently maps to daily live notional cap (`counters.liveNotionalUsdc`).
85
+ - `max_open_markets` currently maps to daily live operation cap (`counters.liveOps`).
86
+ - All risk state and panic files are persisted with hardened permissions under `~/.pandora/`.
87
+
66
88
  ## Fork Mode Notes
67
89
 
68
90
  - Runtime marker is included in payloads: `data.runtime.mode = "fork" | "live"`.
@@ -85,21 +107,25 @@ pandora --output json sports resolve plan --event-id <event-id> --poll-address <
85
107
  - `pandora markets list|get`
86
108
  - `pandora sports books list`
87
109
  - `pandora sports events list|live`
88
- - `pandora sports odds snapshot`
110
+ - `pandora sports odds snapshot|bulk`
89
111
  - `pandora sports consensus`
90
112
  - `pandora sports create plan|run`
91
113
  - `pandora sports sync once|run|start|stop|status`
92
114
  - `pandora sports resolve plan`
115
+ - `pandora lifecycle start|status|resolve`
93
116
  - `pandora quote`
94
117
  - `pandora trade`
95
118
  - `pandora history`
96
119
  - `pandora export`
97
120
  - `pandora arbitrage`
121
+ - `pandora arb scan --output ndjson`
122
+ - `pandora odds record|history`
98
123
  - `pandora autopilot run|once`
99
124
  - `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close`
100
125
  - `pandora polymarket check|approve|preflight|trade`
101
126
  - `pandora resolve`
102
127
  - `pandora lp add|remove|positions`
128
+ - `pandora risk show|panic`
103
129
  - `pandora stream prices|events`
104
130
  - `pandora schema`
105
131
  - `pandora mcp`
@@ -61,6 +61,17 @@ Prerequisite: Node.js `>=18`.
61
61
  - Build manual resolve recommendation:
62
62
  - `pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>`
63
63
 
64
+ ## Quickstart (Lifecycle + Risk)
65
+ - Lifecycle config must be JSON in this release:
66
+ - `pandora --output json lifecycle start --config ./configs/lifecycle.json`
67
+ - `pandora --output json lifecycle status --id <lifecycle-id>`
68
+ - `pandora --output json lifecycle resolve --id <lifecycle-id> --confirm`
69
+ - Risk controls:
70
+ - `pandora --output json risk show`
71
+ - `pandora --output json risk panic --reason "incident"`
72
+ - `pandora --output json risk panic --clear`
73
+ - semantic note: `max_daily_loss_usd` and `max_open_markets` are enforced as daily live-notional / daily operation counters in current implementation.
74
+
64
75
  ## New CLI capabilities
65
76
  - Global machine-readable output:
66
77
  - `pandora --output json doctor`
@@ -130,6 +141,7 @@ Prerequisite: Node.js `>=18`.
130
141
  ### MCP server (`pandora mcp`)
131
142
  - Runs MCP server over stdio with tool discovery + execution.
132
143
  - `tools/list` includes JSON-capable command tools (for example `markets.list`, `trade`, `mirror.plan`, `polymarket.check`).
144
+ - Also includes `odds.history|record` and `lifecycle.status|start|resolve` tools.
133
145
  - `launch` and `clone-bet` are intentionally not exposed over MCP because they stream script output.
134
146
  - MCP safety rails:
135
147
  - mutating tools require explicit execute intent (`intent.execute=true`) for live execution.
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pandora-cli-skills
3
3
  summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
4
- version: 1.1.40
4
+ version: 1.1.42
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -72,6 +72,14 @@ npm link
72
72
  - RPC reachability and chain id match
73
73
  - bytecode checks for `ORACLE` + `FACTORY` (`--check-usdc-code` optional)
74
74
 
75
+ ## Lifecycle and risk notes
76
+ - `lifecycle start --config` expects a JSON object file in the current release.
77
+ - `risk show|panic` controls a global execution lock used by live write paths.
78
+ - Current risk-counter semantics:
79
+ - `max_position_usd` guards per-operation notional.
80
+ - `max_daily_loss_usd` is enforced as daily live notional (`counters.liveNotionalUsdc`).
81
+ - `max_open_markets` is enforced as daily live operation count (`counters.liveOps`).
82
+
75
83
  ## Complete command + flag reference (authoritative)
76
84
  This section mirrors live CLI help output so agent runs can rely on one source of truth.
77
85
 
@@ -83,7 +91,11 @@ pandora [--output table|json] doctor [--dotenv-path <path>] [--skip-dotenv] [--c
83
91
  pandora [--output table|json] setup [--force] [--dotenv-path <path>] [--example <path>] [--check-usdc-code] [--check-polymarket] [--rpc-timeout-ms <ms>]
84
92
  pandora [--output table|json] markets list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--poll-address <address>] [--market-type <type>] [--where-json <json>] [--active|--resolved|--expiring-soon] [--expiring-hours <n>] [--expand] [--with-odds]
85
93
  pandora [--output table|json] markets get [--id <id> ...] [--stdin]
86
- pandora [--output table|json] sports books list|events list|events live|odds snapshot|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
94
+ pandora [--output table|json] sports books list|events list|events live|odds snapshot|odds bulk|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
95
+ pandora [--output table|json] lifecycle start --config <path>|status --id <id>|resolve --id <id> --confirm
96
+ pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--iterations <n>] [--interval-ms <ms>]
97
+ pandora [--output table|json] odds record --competition <id> --interval <sec> [--max-samples <n>] [--event-id <id>] [--venues pandora_amm,polymarket]
98
+ pandora [--output table|json] odds history --event-id <id> --output csv|json [--limit <n>]
87
99
  pandora [--output table|json] polls list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--status <int>] [--category <int>] [--question-contains <text>] [--where-json <json>]
88
100
  pandora [--output table|json] polls get --id <id>
89
101
  pandora [--output table|json] events list [--type all|liquidity|oracle-fee|claim] [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-direction asc|desc] [--chain-id <id>] [--wallet <address>] [--market-address <address>] [--poll-address <address>] [--tx-hash <hash>]
@@ -106,6 +118,7 @@ pandora [--output table|json] analyze --market-address <address> [--provider <na
106
118
  pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
107
119
  pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
108
120
  pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
121
+ pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
109
122
  pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
110
123
  pandora [--output json] schema
111
124
  pandora mcp
@@ -152,9 +165,10 @@ trade --condition-id <id>|--slug <slug>|--token-id <id> --token yes|no --amount-
152
165
  | `sports events list` | List normalized soccer events. | `--competition`, `--kickoff-after`, `--kickoff-before`, `--limit` |
153
166
  | `sports events live` | List only live/in-play events. | `--competition`, `--limit`, `--provider` |
154
167
  | `sports odds snapshot` | Fetch event odds snapshot plus consensus context. | `--event-id`, `--trim-percent`, `--min-tier1-books`, `--min-total-books` |
168
+ | `sports odds bulk` | Fetch all odds for a competition and refresh local cache. | `--competition`, `--provider`, `--timeout-ms`, `--limit` |
155
169
  | `sports consensus` | Compute trimmed-median consensus from live or offline checks. | `--event-id` or `--checks-json`, `--trim-percent`, `--book-priority` |
156
170
  | `sports create plan` | Build conservative creation plan and safety gates. | `--event-id`, `--selection`, `--market-type`, `--creation-window-open-min`, `--creation-window-close-min` |
157
- | `sports create run` | Dry-run or execute creation path. | `--event-id`, `--dry-run/--execute`, `--liquidity-usdc`, `--chain-id`, `--rpc-url` |
171
+ | `sports create run` | Dry-run or execute creation path. | `--event-id`, `--dry-run/--execute`, `--liquidity-usdc`, `--chain-id`, `--rpc-url`, `--model-file`, `--model-stdin` |
158
172
  | `sports sync once|run|start|stop|status` | Evaluate and operate sports sync runtime state. | `--event-id` (required for `once|run|start`), `--risk-profile`, `--state-file`, `--paper/--execute-live` |
159
173
  | `sports resolve plan` | Build manual-final resolution recommendation. | `--event-id` or `--checks-json/--checks-file`, `--poll-address`, `--settle-delay-ms`, `--consecutive-checks-required` |
160
174
 
@@ -0,0 +1,336 @@
1
+ const ARB_MARKET_FIELDS = [
2
+ 'id',
3
+ 'chainId',
4
+ 'chainName',
5
+ 'pollAddress',
6
+ 'marketCloseTimestamp',
7
+ 'yesChance',
8
+ 'yesPct',
9
+ 'reserveYes',
10
+ 'reserveNo',
11
+ 'totalVolume',
12
+ 'currentTvl',
13
+ 'createdAt',
14
+ ];
15
+
16
+ const ARB_USAGE =
17
+ 'pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
18
+
19
+ function requireDep(deps, name) {
20
+ if (!deps || typeof deps[name] !== 'function') {
21
+ throw new Error(`createRunArbCommand requires deps.${name}()`);
22
+ }
23
+ return deps[name];
24
+ }
25
+
26
+ function roundNumber(value, digits = 6) {
27
+ const multiplier = 10 ** digits;
28
+ return Math.round(Number(value) * multiplier) / multiplier;
29
+ }
30
+
31
+ function toFiniteNumber(value) {
32
+ const parsed = Number(value);
33
+ if (!Number.isFinite(parsed)) return null;
34
+ return parsed;
35
+ }
36
+
37
+ function normalizeYesPct(market) {
38
+ if (!market || typeof market !== 'object') return null;
39
+
40
+ const explicit = toFiniteNumber(market.yesPct);
41
+ if (explicit !== null) {
42
+ if (explicit >= 0 && explicit <= 100) {
43
+ return explicit;
44
+ }
45
+ if (explicit >= 0 && explicit <= 1) {
46
+ return explicit * 100;
47
+ }
48
+ }
49
+
50
+ const chance = toFiniteNumber(market.yesChance);
51
+ if (chance !== null) {
52
+ if (chance >= 0 && chance <= 1) {
53
+ return chance * 100;
54
+ }
55
+ if (chance >= 0 && chance <= 100) {
56
+ return chance;
57
+ }
58
+ }
59
+
60
+ const reserveYes = toFiniteNumber(market.reserveYes);
61
+ const reserveNo = toFiniteNumber(market.reserveNo);
62
+ if (reserveYes !== null && reserveNo !== null) {
63
+ const total = reserveYes + reserveNo;
64
+ if (total > 0) {
65
+ return (reserveNo / total) * 100;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function buildMarketSnapshots(markets, orderedIds) {
73
+ const byId = new Map((Array.isArray(markets) ? markets : []).map((item) => [String(item && item.id), item]));
74
+
75
+ return orderedIds
76
+ .map((id) => {
77
+ const market = byId.get(id) || null;
78
+ return {
79
+ id,
80
+ market,
81
+ yesPct: market ? normalizeYesPct(market) : null,
82
+ };
83
+ })
84
+ .filter((item) => item.market && Number.isFinite(item.yesPct));
85
+ }
86
+
87
+ /**
88
+ * Build deterministic pairwise arbitrage opportunities across provided markets.
89
+ * @param {object} options
90
+ * @returns {object[]}
91
+ */
92
+ function buildArbOpportunities(options) {
93
+ const snapshots = Array.isArray(options.marketSnapshots) ? options.marketSnapshots : [];
94
+ const minNetSpreadPct = Number.isFinite(options.minNetSpreadPct) ? Number(options.minNetSpreadPct) : 0;
95
+ const feePctPerLeg = Number.isFinite(options.feePctPerLeg) ? Number(options.feePctPerLeg) : 0;
96
+ const amountUsdc = Number.isFinite(options.amountUsdc) ? Number(options.amountUsdc) : 0;
97
+
98
+ const opportunities = [];
99
+ for (let leftIndex = 0; leftIndex < snapshots.length; leftIndex += 1) {
100
+ for (let rightIndex = leftIndex + 1; rightIndex < snapshots.length; rightIndex += 1) {
101
+ const left = snapshots[leftIndex];
102
+ const right = snapshots[rightIndex];
103
+ if (!Number.isFinite(left.yesPct) || !Number.isFinite(right.yesPct)) {
104
+ continue;
105
+ }
106
+
107
+ const buyYes = left.yesPct <= right.yesPct ? left : right;
108
+ const buyNo = buyYes === left ? right : left;
109
+ const grossSpreadPct = roundNumber(buyNo.yesPct - buyYes.yesPct, 6);
110
+ const netSpreadPct = roundNumber(grossSpreadPct - feePctPerLeg * 2, 6);
111
+ if (netSpreadPct <= 0 || netSpreadPct < minNetSpreadPct) {
112
+ continue;
113
+ }
114
+
115
+ const profitUsdc = roundNumber((amountUsdc * netSpreadPct) / 100, 6);
116
+ opportunities.push({
117
+ pair: `${buyYes.id}|${buyNo.id}`,
118
+ buyYesMarket: buyYes.id,
119
+ buyNoMarket: buyNo.id,
120
+ buyYesPct: roundNumber(buyYes.yesPct, 6),
121
+ buyNoPct: roundNumber(buyNo.yesPct, 6),
122
+ grossSpreadPct,
123
+ netSpreadPct,
124
+ netSpread: roundNumber(netSpreadPct / 100, 8),
125
+ amountUsdc: roundNumber(amountUsdc, 6),
126
+ profitUsdc,
127
+ profit: profitUsdc,
128
+ });
129
+ }
130
+ }
131
+
132
+ opportunities.sort((left, right) => {
133
+ if (right.netSpreadPct !== left.netSpreadPct) {
134
+ return right.netSpreadPct - left.netSpreadPct;
135
+ }
136
+ return left.pair.localeCompare(right.pair);
137
+ });
138
+
139
+ return opportunities;
140
+ }
141
+
142
+ /**
143
+ * Parse `arb scan` command flags.
144
+ * @param {string[]} args
145
+ * @param {object} deps
146
+ * @returns {object}
147
+ */
148
+ function parseArbScanFlags(args, deps) {
149
+ const { CliError, requireFlagValue, parseCsvList, parseNumber, parsePositiveNumber, parsePositiveInteger } = deps;
150
+
151
+ const action = args[0];
152
+ if (!action || action !== 'scan') {
153
+ throw new CliError('INVALID_ARGS', 'arb requires subcommand: scan.');
154
+ }
155
+
156
+ const options = {
157
+ action,
158
+ markets: [],
159
+ output: 'ndjson',
160
+ minNetSpreadPct: 0,
161
+ feePctPerLeg: 0,
162
+ amountUsdc: 100,
163
+ intervalMs: 5_000,
164
+ iterations: null,
165
+ };
166
+
167
+ const rest = args.slice(1);
168
+ for (let i = 0; i < rest.length; i += 1) {
169
+ const token = rest[i];
170
+ if (token === '--markets') {
171
+ options.markets = parseCsvList(requireFlagValue(rest, i, '--markets'), '--markets');
172
+ i += 1;
173
+ continue;
174
+ }
175
+ if (token === '--output') {
176
+ options.output = String(requireFlagValue(rest, i, '--output')).trim().toLowerCase();
177
+ i += 1;
178
+ continue;
179
+ }
180
+ if (token === '--min-net-spread-pct') {
181
+ options.minNetSpreadPct = parseNumber(requireFlagValue(rest, i, '--min-net-spread-pct'), '--min-net-spread-pct');
182
+ i += 1;
183
+ continue;
184
+ }
185
+ if (token === '--fee-pct-per-leg') {
186
+ options.feePctPerLeg = parseNumber(requireFlagValue(rest, i, '--fee-pct-per-leg'), '--fee-pct-per-leg');
187
+ i += 1;
188
+ continue;
189
+ }
190
+ if (token === '--amount-usdc') {
191
+ options.amountUsdc = parsePositiveNumber(requireFlagValue(rest, i, '--amount-usdc'), '--amount-usdc');
192
+ i += 1;
193
+ continue;
194
+ }
195
+ if (token === '--interval-ms') {
196
+ options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
197
+ i += 1;
198
+ continue;
199
+ }
200
+ if (token === '--iterations') {
201
+ options.iterations = parsePositiveInteger(requireFlagValue(rest, i, '--iterations'), '--iterations');
202
+ i += 1;
203
+ continue;
204
+ }
205
+
206
+ throw new CliError('UNKNOWN_FLAG', `Unknown flag for arb scan: ${token}`);
207
+ }
208
+
209
+ if (!Array.isArray(options.markets) || options.markets.length < 2) {
210
+ throw new CliError('MISSING_REQUIRED_FLAG', 'arb scan requires at least two markets via --markets <csv>.');
211
+ }
212
+
213
+ if (options.output !== 'ndjson') {
214
+ throw new CliError('INVALID_FLAG_VALUE', 'arb scan currently supports only --output ndjson.');
215
+ }
216
+
217
+ if (options.minNetSpreadPct < 0) {
218
+ throw new CliError('INVALID_FLAG_VALUE', '--min-net-spread-pct must be >= 0.');
219
+ }
220
+
221
+ if (options.feePctPerLeg < 0) {
222
+ throw new CliError('INVALID_FLAG_VALUE', '--fee-pct-per-leg must be >= 0.');
223
+ }
224
+
225
+ options.markets = Array.from(
226
+ new Set(
227
+ options.markets
228
+ .map((item) => String(item).trim())
229
+ .filter(Boolean),
230
+ ),
231
+ );
232
+
233
+ if (options.markets.length < 2) {
234
+ throw new CliError('INVALID_ARGS', 'arb scan requires at least two distinct market ids.');
235
+ }
236
+
237
+ return options;
238
+ }
239
+
240
+ async function fetchMarketsById(indexerUrl, marketIds, timeoutMs, deps) {
241
+ const query = deps.buildGraphqlGetQuery('markets', ARB_MARKET_FIELDS);
242
+ const requests = marketIds.map(async (id) => {
243
+ const data = await deps.graphqlRequest(indexerUrl, query, { id }, timeoutMs);
244
+ return data && data.markets ? data.markets : null;
245
+ });
246
+ return Promise.all(requests);
247
+ }
248
+
249
+ /**
250
+ * Create runner for `pandora arb` commands.
251
+ * @param {object} deps
252
+ * @returns {(args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}
253
+ */
254
+ function createRunArbCommand(deps) {
255
+ const CliError = requireDep(deps, 'CliError');
256
+ const includesHelpFlag = requireDep(deps, 'includesHelpFlag');
257
+ const emitSuccess = requireDep(deps, 'emitSuccess');
258
+ const commandHelpPayload = requireDep(deps, 'commandHelpPayload');
259
+ const parseIndexerSharedFlags = requireDep(deps, 'parseIndexerSharedFlags');
260
+ const maybeLoadIndexerEnv = requireDep(deps, 'maybeLoadIndexerEnv');
261
+ const resolveIndexerUrl = requireDep(deps, 'resolveIndexerUrl');
262
+ const requireFlagValue = requireDep(deps, 'requireFlagValue');
263
+ const parseCsvList = requireDep(deps, 'parseCsvList');
264
+ const parseNumber = requireDep(deps, 'parseNumber');
265
+ const parsePositiveNumber = requireDep(deps, 'parsePositiveNumber');
266
+ const parsePositiveInteger = requireDep(deps, 'parsePositiveInteger');
267
+ const buildGraphqlGetQuery = requireDep(deps, 'buildGraphqlGetQuery');
268
+ const graphqlRequest = requireDep(deps, 'graphqlRequest');
269
+ const sleepMs = requireDep(deps, 'sleepMs');
270
+
271
+ return async function runArbCommand(args, context) {
272
+ const shared = parseIndexerSharedFlags(args);
273
+ if (!shared.rest.length || includesHelpFlag(shared.rest)) {
274
+ if (context.outputMode === 'json') {
275
+ emitSuccess(context.outputMode, 'arb.help', commandHelpPayload(ARB_USAGE));
276
+ } else {
277
+ // eslint-disable-next-line no-console
278
+ console.log(`Usage: ${ARB_USAGE}`);
279
+ }
280
+ return;
281
+ }
282
+
283
+ maybeLoadIndexerEnv(shared);
284
+ const indexerUrl = resolveIndexerUrl(shared.indexerUrl);
285
+ const options = parseArbScanFlags(shared.rest, {
286
+ CliError,
287
+ requireFlagValue,
288
+ parseCsvList,
289
+ parseNumber,
290
+ parsePositiveNumber,
291
+ parsePositiveInteger,
292
+ });
293
+
294
+ const maxIterations = Number.isInteger(options.iterations) ? options.iterations : Number.POSITIVE_INFINITY;
295
+ let iteration = 0;
296
+
297
+ while (iteration < maxIterations) {
298
+ iteration += 1;
299
+ const markets = await fetchMarketsById(indexerUrl, options.markets, shared.timeoutMs, {
300
+ buildGraphqlGetQuery,
301
+ graphqlRequest,
302
+ });
303
+ const snapshots = buildMarketSnapshots(markets, options.markets);
304
+ const opportunities = buildArbOpportunities({
305
+ marketSnapshots: snapshots,
306
+ minNetSpreadPct: options.minNetSpreadPct,
307
+ feePctPerLeg: options.feePctPerLeg,
308
+ amountUsdc: options.amountUsdc,
309
+ });
310
+
311
+ for (const opportunity of opportunities) {
312
+ // eslint-disable-next-line no-console
313
+ console.log(
314
+ JSON.stringify({
315
+ type: 'arb.scan.opportunity',
316
+ timestamp: new Date().toISOString(),
317
+ iteration,
318
+ indexerUrl,
319
+ ...opportunity,
320
+ }),
321
+ );
322
+ }
323
+
324
+ if (iteration < maxIterations) {
325
+ await sleepMs(options.intervalMs);
326
+ }
327
+ }
328
+ };
329
+ }
330
+
331
+ module.exports = {
332
+ ARB_USAGE,
333
+ buildArbOpportunities,
334
+ createRunArbCommand,
335
+ parseArbScanFlags,
336
+ };
@@ -19,6 +19,9 @@ function createCommandRouter(deps = {}) {
19
19
  runMarketsCommand,
20
20
  runScanCommand,
21
21
  runSportsCommand,
22
+ runLifecycleCommand,
23
+ runArbCommand,
24
+ runOddsCommand,
22
25
  runQuoteCommand,
23
26
  runTradeCommand,
24
27
  runPollsCommand,
@@ -38,6 +41,7 @@ function createCommandRouter(deps = {}) {
38
41
  runSuggestCommand,
39
42
  runResolveCommand,
40
43
  runLpCommand,
44
+ runRiskCommand,
41
45
  runMcpCommand,
42
46
  runStreamCommand,
43
47
  runScriptCommand,
@@ -67,6 +71,9 @@ function createCommandRouter(deps = {}) {
67
71
  requireFn('runMarketsCommand', runMarketsCommand);
68
72
  requireFn('runScanCommand', runScanCommand);
69
73
  requireFn('runSportsCommand', runSportsCommand);
74
+ requireFn('runLifecycleCommand', runLifecycleCommand);
75
+ requireFn('runArbCommand', runArbCommand);
76
+ requireFn('runOddsCommand', runOddsCommand);
70
77
  requireFn('runQuoteCommand', runQuoteCommand);
71
78
  requireFn('runTradeCommand', runTradeCommand);
72
79
  requireFn('runPollsCommand', runPollsCommand);
@@ -86,6 +93,7 @@ function createCommandRouter(deps = {}) {
86
93
  requireFn('runSuggestCommand', runSuggestCommand);
87
94
  requireFn('runResolveCommand', runResolveCommand);
88
95
  requireFn('runLpCommand', runLpCommand);
96
+ requireFn('runRiskCommand', runRiskCommand);
89
97
  requireFn('runMcpCommand', runMcpCommand);
90
98
  requireFn('runStreamCommand', runStreamCommand);
91
99
  requireFn('runSchemaCommand', deps.runSchemaCommand);
@@ -156,6 +164,9 @@ function createCommandRouter(deps = {}) {
156
164
  markets: async (handlerArgs, handlerContext) => runMarketsCommand(handlerArgs, handlerContext),
157
165
  scan: async (handlerArgs, handlerContext) => runScanCommand(handlerArgs, handlerContext),
158
166
  sports: async (handlerArgs, handlerContext) => runSportsCommand(handlerArgs, handlerContext),
167
+ lifecycle: async (handlerArgs, handlerContext) => runLifecycleCommand(handlerArgs, handlerContext),
168
+ arb: async (handlerArgs, handlerContext) => runArbCommand(handlerArgs, handlerContext),
169
+ odds: async (handlerArgs, handlerContext) => runOddsCommand(handlerArgs, handlerContext),
159
170
  quote: async (handlerArgs, handlerContext) => runQuoteCommand(handlerArgs, handlerContext),
160
171
  trade: async (handlerArgs, handlerContext) => runTradeCommand(handlerArgs, handlerContext),
161
172
  polls: async (handlerArgs, handlerContext) => runPollsCommand(handlerArgs, handlerContext),
@@ -175,6 +186,7 @@ function createCommandRouter(deps = {}) {
175
186
  suggest: async (handlerArgs, handlerContext) => runSuggestCommand(handlerArgs, handlerContext),
176
187
  resolve: async (handlerArgs, handlerContext) => runResolveCommand(handlerArgs, handlerContext),
177
188
  lp: async (handlerArgs, handlerContext) => runLpCommand(handlerArgs, handlerContext),
189
+ risk: async (handlerArgs, handlerContext) => runRiskCommand(handlerArgs, handlerContext),
178
190
  mcp: async (handlerArgs, handlerContext) => {
179
191
  if (handlerContext.outputMode === 'json') {
180
192
  throw new CliError(