pandora-cli-skills 1.1.43 → 1.1.44

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 (63) hide show
  1. package/README.md +19 -0
  2. package/README_FOR_SHARING.md +21 -0
  3. package/SKILL.md +27 -4
  4. package/cli/lib/arb_command_service.cjs +175 -9
  5. package/cli/lib/arbitrage_service.cjs +187 -3
  6. package/cli/lib/brier_score_service.cjs +271 -0
  7. package/cli/lib/command_router.cjs +6 -0
  8. package/cli/lib/error_recovery_service.cjs +85 -0
  9. package/cli/lib/forecast_store.cjs +361 -0
  10. package/cli/lib/mcp_tool_registry.cjs +52 -1
  11. package/cli/lib/mirror_command_service.cjs +3 -3
  12. package/cli/lib/mirror_econ_service.cjs +409 -1
  13. package/cli/lib/mirror_handlers/deploy.cjs +2 -2
  14. package/cli/lib/mirror_handlers/simulate.cjs +1 -1
  15. package/cli/lib/model_command_service.cjs +99 -0
  16. package/cli/lib/model_diagnose_service.cjs +204 -0
  17. package/cli/lib/model_handlers/calibrate.cjs +233 -0
  18. package/cli/lib/model_handlers/correlation.cjs +458 -0
  19. package/cli/lib/model_handlers/diagnose.cjs +47 -0
  20. package/cli/lib/model_handlers/score_brier.cjs +119 -0
  21. package/cli/lib/model_store.cjs +292 -0
  22. package/cli/lib/pandora_deploy_service.cjs +4 -2
  23. package/cli/lib/parsers/mirror_deploy_flags.cjs +7 -2
  24. package/cli/lib/parsers/mirror_go_flags.cjs +7 -2
  25. package/cli/lib/parsers/mirror_hedge_calc_flags.cjs +7 -2
  26. package/cli/lib/parsers/mirror_remaining_flags.cjs +64 -2
  27. package/cli/lib/parsers/model_flags.cjs +446 -0
  28. package/cli/lib/parsers/simulate_flags.cjs +310 -0
  29. package/cli/lib/parsers/watch_flags.cjs +64 -0
  30. package/cli/lib/quant/abm_market.cjs +282 -0
  31. package/cli/lib/quant/copula.cjs +407 -0
  32. package/cli/lib/quant/importance_sampling.cjs +224 -0
  33. package/cli/lib/quant/mc_stats.cjs +186 -0
  34. package/cli/lib/quant/particle_filter.cjs +305 -0
  35. package/cli/lib/quant/rng.cjs +214 -0
  36. package/cli/lib/quant/variance_reduction.cjs +156 -0
  37. package/cli/lib/schema_command_service.cjs +138 -2
  38. package/cli/lib/shared/constants.cjs +6 -0
  39. package/cli/lib/simulate_command_service.cjs +140 -0
  40. package/cli/lib/simulate_handlers/agents.cjs +194 -0
  41. package/cli/lib/simulate_handlers/common.cjs +361 -0
  42. package/cli/lib/simulate_handlers/mc.cjs +212 -0
  43. package/cli/lib/simulate_handlers/particle_filter.cjs +370 -0
  44. package/cli/lib/watch_command_service.cjs +114 -0
  45. package/cli/pandora.cjs +120 -3
  46. package/package.json +2 -2
  47. package/references/creation-script.md +1 -1
  48. package/tests/cli/cli.integration.test.cjs +464 -2
  49. package/tests/cli/mcp.integration.test.cjs +100 -0
  50. package/tests/unit/abm_market.test.cjs +226 -0
  51. package/tests/unit/brier_scoring.test.cjs +74 -0
  52. package/tests/unit/brier_watch_command.test.cjs +121 -0
  53. package/tests/unit/combinatorial_arb.test.cjs +171 -0
  54. package/tests/unit/mirror_simulate_mc.test.cjs +152 -0
  55. package/tests/unit/model_calibrate.test.cjs +142 -0
  56. package/tests/unit/model_correlation.test.cjs +147 -0
  57. package/tests/unit/model_diagnose.test.cjs +83 -0
  58. package/tests/unit/quant_core.test.cjs +104 -0
  59. package/tests/unit/quant_models.test.cjs +85 -0
  60. package/tests/unit/quant_stores.test.cjs +134 -0
  61. package/tests/unit/simulate_command_service.test.cjs +91 -0
  62. package/tests/unit/simulate_flags.test.cjs +185 -0
  63. package/tests/unit/simulate_handlers.test.cjs +103 -0
package/README.md CHANGED
@@ -130,6 +130,25 @@ pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
130
130
  - `pandora schema`
131
131
  - `pandora mcp`
132
132
 
133
+ ## Quant ABM Baseline (Module Contract)
134
+
135
+ - Deterministic ABM engine: `cli/lib/quant/abm_market.cjs`.
136
+ - Simulate-agents handler module: `cli/lib/simulate_handlers/agents.cjs`.
137
+ - Supported handler flags:
138
+ - `--n-informed` or `--n_informed`
139
+ - `--n-noise` or `--n_noise`
140
+ - `--n-mm` or `--n_mm`
141
+ - `--n-steps` or `--n_steps`
142
+ - `--seed`
143
+ - ABM payload fields include:
144
+ - `convergenceError`
145
+ - `spreadTrajectory[]`
146
+ - `volume` (`total`, `averagePerStep`, `byAgentType`)
147
+ - `pnlByAgentType`
148
+ - `runtimeBounds` (`complexity`, `estimatedAgentDecisions`, `estimatedWorkUnits`)
149
+ - Runtime bound metadata is documented as `O(n_steps * (n_informed + n_noise))`.
150
+ - Unit coverage for this baseline is in `tests/unit/abm_market.test.cjs`.
151
+
133
152
  ## Docs
134
153
 
135
154
  - Full command contract and workflows: [`SKILL.md`](./SKILL.md)
@@ -136,6 +136,27 @@ Prerequisite: Node.js `>=18`.
136
136
  - `pandora resolve`
137
137
  - `pandora lp add|remove|positions`
138
138
 
139
+ ## Quant ABM baseline (current implementation)
140
+ - Deterministic ABM engine module:
141
+ - `cli/lib/quant/abm_market.cjs`
142
+ - Simulate-agents handler module:
143
+ - `cli/lib/simulate_handlers/agents.cjs`
144
+ - Handler parser accepts:
145
+ - `--n-informed|--n_informed`
146
+ - `--n-noise|--n_noise`
147
+ - `--n-mm|--n_mm`
148
+ - `--n-steps|--n_steps`
149
+ - `--seed`
150
+ - ABM output fields include:
151
+ - `convergenceError`
152
+ - `spreadTrajectory[]`
153
+ - `volume` (`total`, `averagePerStep`, `byAgentType`)
154
+ - `pnlByAgentType`
155
+ - `runtimeBounds` with complexity `O(n_steps * (n_informed + n_noise))`
156
+ - Coverage notes:
157
+ - `tests/unit/abm_market.test.cjs` validates deterministic seed behavior, required ABM metrics, parser/handler behavior, and runtime-bound calculations.
158
+ - `package.json` `test:unit` includes the ABM suite in default unit execution.
159
+
139
160
  ## Agent-native expansion details
140
161
 
141
162
  ### MCP server (`pandora mcp`)
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.43
4
+ version: 1.1.44
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -80,6 +80,29 @@ npm link
80
80
  - `max_daily_loss_usd` is enforced as daily live notional (`counters.liveNotionalUsdc`).
81
81
  - `max_open_markets` is enforced as daily live operation count (`counters.liveOps`).
82
82
 
83
+ ## Quant ABM baseline (module contract)
84
+ - Deterministic ABM core:
85
+ - `cli/lib/quant/abm_market.cjs`
86
+ - Simulate-agents handler:
87
+ - `cli/lib/simulate_handlers/agents.cjs`
88
+ - Handler flags:
89
+ - `--n-informed|--n_informed`
90
+ - `--n-noise|--n_noise`
91
+ - `--n-mm|--n_mm`
92
+ - `--n-steps|--n_steps`
93
+ - `--seed`
94
+ - ABM payload fields:
95
+ - `convergenceError`
96
+ - `spreadTrajectory[]`
97
+ - `volume` (`total`, `averagePerStep`, `byAgentType`)
98
+ - `pnlByAgentType`
99
+ - `runtimeBounds` (`complexity`, `estimatedAgentDecisions`, `estimatedWorkUnits`)
100
+ - Runtime complexity metadata: `O(n_steps * (n_informed + n_noise))`.
101
+ - Unit coverage:
102
+ - `tests/unit/abm_market.test.cjs`
103
+ - Note:
104
+ - This section documents the current module + handler contract. Top-level simulate namespace routing can consume this handler when wired by command service.
105
+
83
106
  ## Complete command + flag reference (authoritative)
84
107
  This section mirrors live CLI help output so agent runs can rely on one source of truth.
85
108
 
@@ -131,12 +154,12 @@ Mirror subcommand detail:
131
154
  ```text
132
155
  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>]
133
156
  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>]
134
- 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...>] [--min-close-lead-seconds <n>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]
157
+ deploy --plan-file <path>|--polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--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...>] [--min-close-lead-seconds <n>] [--manifest-file <path>] [--polymarket-host <url>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]
135
158
  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>]
136
159
  lp-explain --liquidity-usdc <n> [--source-yes-pct <0-100>] [--distribution-yes <parts>] [--distribution-no <parts>]
137
160
  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>]
138
- 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>]
139
- 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>]
161
+ simulate --liquidity-usdc <n> [--source-yes-pct <0-100>] [--target-yes-pct <0-100>] [--distribution-yes <parts>] [--distribution-no <parts>] [--fee-tier <500-50000>] [--volume-scenarios <csv>] [--hedge-ratio <n>] [--hedge-cost-bps <n>] [--polymarket-yes-pct <0-100>]
162
+ go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier <500-50000>] [--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>]
140
163
  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>]
141
164
  status --state-file <path>|--strategy-hash <hash> [--with-live] [--pandora-market-address <address>|--market-address <address>] [--polymarket-market-id <id>|--polymarket-slug <slug>]
142
165
  close --pandora-market-address <address>|--market-address <address> --polymarket-market-id <id>|--polymarket-slug <slug> --dry-run|--execute
@@ -14,7 +14,7 @@ const ARB_MARKET_FIELDS = [
14
14
  ];
15
15
 
16
16
  const ARB_USAGE =
17
- 'pandora arb scan --markets <csv> --output ndjson|json [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
17
+ 'pandora arb scan --markets <csv> --output ndjson|json [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
18
18
 
19
19
  function requireDep(deps, name) {
20
20
  if (!deps || typeof deps[name] !== 'function') {
@@ -139,6 +139,114 @@ function buildArbOpportunities(options) {
139
139
  return opportunities;
140
140
  }
141
141
 
142
+ function enumerateCombinations(items, size, onCombination) {
143
+ if (!Array.isArray(items) || !Number.isInteger(size) || size < 1 || size > items.length) return;
144
+ if (typeof onCombination !== 'function') return;
145
+
146
+ const selected = [];
147
+ function walk(startIndex) {
148
+ if (selected.length === size) {
149
+ onCombination(selected.slice());
150
+ return;
151
+ }
152
+ const remaining = size - selected.length;
153
+ const maxStart = items.length - remaining;
154
+ for (let index = startIndex; index <= maxStart; index += 1) {
155
+ selected.push(items[index]);
156
+ walk(index + 1);
157
+ selected.pop();
158
+ }
159
+ }
160
+ walk(0);
161
+ }
162
+
163
+ /**
164
+ * Build bundle-level combinatorial opportunities across provided markets.
165
+ * @param {object} options
166
+ * @returns {object[]}
167
+ */
168
+ function buildCombinatorialArbOpportunities(options) {
169
+ const marketSnapshots = Array.isArray(options.marketSnapshots)
170
+ ? options.marketSnapshots.filter((item) => item && Number.isFinite(item.yesPct))
171
+ : [];
172
+ if (marketSnapshots.length < 3) return [];
173
+
174
+ const minNetSpreadPct = Number.isFinite(options.minNetSpreadPct) ? Number(options.minNetSpreadPct) : 0;
175
+ const feePctPerLeg = Number.isFinite(options.feePctPerLeg) ? Number(options.feePctPerLeg) : 0;
176
+ const slippagePctPerLeg = Number.isFinite(options.slippagePctPerLeg) ? Number(options.slippagePctPerLeg) : 0;
177
+ const amountUsdc = Number.isFinite(options.amountUsdc) ? Number(options.amountUsdc) : 0;
178
+ const requestedMaxBundleSize = Number.isInteger(options.maxBundleSize) ? options.maxBundleSize : 4;
179
+ const maxBundleSize = Math.max(3, Math.min(requestedMaxBundleSize, marketSnapshots.length));
180
+
181
+ const opportunities = [];
182
+ for (let bundleSize = 3; bundleSize <= maxBundleSize; bundleSize += 1) {
183
+ enumerateCombinations(marketSnapshots, bundleSize, (bundle) => {
184
+ const bundleMarketIds = bundle.map((item) => item.id);
185
+ const sumYesPct = roundNumber(bundle.reduce((total, item) => total + Number(item.yesPct), 0), 6);
186
+ const sumNoPct = roundNumber(bundle.reduce((total, item) => total + (100 - Number(item.yesPct)), 0), 6);
187
+ const feeImpactPct = roundNumber(bundleSize * feePctPerLeg, 6);
188
+ const slippageImpactPct = roundNumber(bundleSize * slippagePctPerLeg, 6);
189
+
190
+ const evaluate = (strategy) => {
191
+ const grossEdgePct = strategy === 'buy_yes_bundle' ? roundNumber(100 - sumYesPct, 6) : roundNumber(sumYesPct - 100, 6);
192
+ if (grossEdgePct <= 0) return;
193
+
194
+ const netSpreadPct = roundNumber(grossEdgePct - feeImpactPct - slippageImpactPct, 6);
195
+ if (netSpreadPct <= 0 || netSpreadPct < minNetSpreadPct) return;
196
+
197
+ const payoutPct = strategy === 'buy_yes_bundle' ? 100 : roundNumber((bundleSize - 1) * 100, 6);
198
+ const totalEntryPct = strategy === 'buy_yes_bundle' ? sumYesPct : sumNoPct;
199
+ const grossProfitUsdc = roundNumber((amountUsdc * grossEdgePct) / 100, 6);
200
+ const profitUsdc = roundNumber((amountUsdc * netSpreadPct) / 100, 6);
201
+
202
+ opportunities.push({
203
+ opportunityType: 'combinatorial',
204
+ strategy,
205
+ pair: `bundle:${bundleMarketIds.join('|')}:${strategy}`,
206
+ bundleMarketIds,
207
+ bundleSize,
208
+ legs: bundle.map((item) => ({
209
+ marketId: item.id,
210
+ yesPct: roundNumber(item.yesPct, 6),
211
+ noPct: roundNumber(100 - item.yesPct, 6),
212
+ })),
213
+ sumYesPct,
214
+ sumNoPct,
215
+ totalEntryPct,
216
+ payoutPct,
217
+ grossSpreadPct: grossEdgePct,
218
+ grossEdgePct,
219
+ feePctPerLeg: roundNumber(feePctPerLeg, 6),
220
+ slippagePctPerLeg: roundNumber(slippagePctPerLeg, 6),
221
+ feeImpactPct,
222
+ slippageImpactPct,
223
+ netSpreadPct,
224
+ netSpread: roundNumber(netSpreadPct / 100, 8),
225
+ amountUsdc: roundNumber(amountUsdc, 6),
226
+ grossProfitUsdc,
227
+ profitUsdc,
228
+ profit: profitUsdc,
229
+ });
230
+ };
231
+
232
+ evaluate('buy_yes_bundle');
233
+ evaluate('buy_no_bundle');
234
+ });
235
+ }
236
+
237
+ opportunities.sort((left, right) => {
238
+ if (right.netSpreadPct !== left.netSpreadPct) {
239
+ return right.netSpreadPct - left.netSpreadPct;
240
+ }
241
+ if (right.bundleSize !== left.bundleSize) {
242
+ return right.bundleSize - left.bundleSize;
243
+ }
244
+ return left.pair.localeCompare(right.pair);
245
+ });
246
+
247
+ return opportunities;
248
+ }
249
+
142
250
  /**
143
251
  * Parse `arb scan` command flags.
144
252
  * @param {string[]} args
@@ -159,7 +267,10 @@ function parseArbScanFlags(args, deps) {
159
267
  output: 'ndjson',
160
268
  minNetSpreadPct: 0,
161
269
  feePctPerLeg: 0,
270
+ slippagePctPerLeg: 0,
162
271
  amountUsdc: 100,
272
+ combinatorial: false,
273
+ maxBundleSize: 4,
163
274
  intervalMs: 5_000,
164
275
  iterations: null,
165
276
  };
@@ -187,11 +298,25 @@ function parseArbScanFlags(args, deps) {
187
298
  i += 1;
188
299
  continue;
189
300
  }
301
+ if (token === '--slippage-pct-per-leg') {
302
+ options.slippagePctPerLeg = parseNumber(requireFlagValue(rest, i, '--slippage-pct-per-leg'), '--slippage-pct-per-leg');
303
+ i += 1;
304
+ continue;
305
+ }
190
306
  if (token === '--amount-usdc') {
191
307
  options.amountUsdc = parsePositiveNumber(requireFlagValue(rest, i, '--amount-usdc'), '--amount-usdc');
192
308
  i += 1;
193
309
  continue;
194
310
  }
311
+ if (token === '--combinatorial') {
312
+ options.combinatorial = true;
313
+ continue;
314
+ }
315
+ if (token === '--max-bundle-size') {
316
+ options.maxBundleSize = parsePositiveInteger(requireFlagValue(rest, i, '--max-bundle-size'), '--max-bundle-size');
317
+ i += 1;
318
+ continue;
319
+ }
195
320
  if (token === '--interval-ms') {
196
321
  options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
197
322
  i += 1;
@@ -222,6 +347,14 @@ function parseArbScanFlags(args, deps) {
222
347
  throw new CliError('INVALID_FLAG_VALUE', '--fee-pct-per-leg must be >= 0.');
223
348
  }
224
349
 
350
+ if (options.slippagePctPerLeg < 0) {
351
+ throw new CliError('INVALID_FLAG_VALUE', '--slippage-pct-per-leg must be >= 0.');
352
+ }
353
+
354
+ if (!Number.isInteger(options.maxBundleSize) || options.maxBundleSize < 3) {
355
+ throw new CliError('INVALID_FLAG_VALUE', '--max-bundle-size must be an integer >= 3.');
356
+ }
357
+
225
358
  options.markets = Array.from(
226
359
  new Set(
227
360
  options.markets
@@ -234,6 +367,10 @@ function parseArbScanFlags(args, deps) {
234
367
  throw new CliError('INVALID_ARGS', 'arb scan requires at least two distinct market ids.');
235
368
  }
236
369
 
370
+ if (options.combinatorial && options.markets.length < 3) {
371
+ throw new CliError('INVALID_ARGS', 'arb scan --combinatorial requires at least three distinct market ids.');
372
+ }
373
+
237
374
  return options;
238
375
  }
239
376
 
@@ -313,7 +450,8 @@ function createRunArbCommand(deps) {
313
450
 
314
451
  const maxIterations = Number.isInteger(options.iterations) ? options.iterations : Number.POSITIVE_INFINITY;
315
452
  let iteration = 0;
316
- const snapshots = [];
453
+ const iterationSnapshots = [];
454
+ let emittedCombinatorialCount = 0;
317
455
 
318
456
  while (iteration < maxIterations) {
319
457
  iteration += 1;
@@ -321,13 +459,30 @@ function createRunArbCommand(deps) {
321
459
  buildGraphqlGetQuery,
322
460
  graphqlRequest,
323
461
  });
324
- const snapshots = buildMarketSnapshots(markets, options.markets);
325
- const opportunities = buildArbOpportunities({
326
- marketSnapshots: snapshots,
462
+ const marketSnapshots = buildMarketSnapshots(markets, options.markets);
463
+ const pairwiseOpportunities = buildArbOpportunities({
464
+ marketSnapshots,
327
465
  minNetSpreadPct: options.minNetSpreadPct,
328
466
  feePctPerLeg: options.feePctPerLeg,
329
467
  amountUsdc: options.amountUsdc,
330
468
  });
469
+ const combinatorialOpportunities = options.combinatorial
470
+ ? buildCombinatorialArbOpportunities({
471
+ marketSnapshots,
472
+ minNetSpreadPct: options.minNetSpreadPct,
473
+ feePctPerLeg: options.feePctPerLeg,
474
+ slippagePctPerLeg: options.slippagePctPerLeg,
475
+ amountUsdc: options.amountUsdc,
476
+ maxBundleSize: options.maxBundleSize,
477
+ })
478
+ : [];
479
+ emittedCombinatorialCount += combinatorialOpportunities.length;
480
+ const opportunities = [...pairwiseOpportunities, ...combinatorialOpportunities].sort((left, right) => {
481
+ if (right.netSpreadPct !== left.netSpreadPct) {
482
+ return right.netSpreadPct - left.netSpreadPct;
483
+ }
484
+ return String(left.pair || '').localeCompare(String(right.pair || ''));
485
+ });
331
486
 
332
487
  if (options.output === 'ndjson') {
333
488
  for (const opportunity of opportunities) {
@@ -343,10 +498,12 @@ function createRunArbCommand(deps) {
343
498
  );
344
499
  }
345
500
  } else {
346
- snapshots.push({
501
+ iterationSnapshots.push({
347
502
  iteration,
348
503
  observedAt: new Date().toISOString(),
349
504
  count: opportunities.length,
505
+ pairwiseCount: pairwiseOpportunities.length,
506
+ combinatorialCount: combinatorialOpportunities.length,
350
507
  opportunities,
351
508
  });
352
509
  }
@@ -357,6 +514,11 @@ function createRunArbCommand(deps) {
357
514
  }
358
515
 
359
516
  if (options.output === 'json') {
517
+ const diagnostics = [];
518
+ if (options.combinatorial && emittedCombinatorialCount === 0) {
519
+ diagnostics.push('No combinatorial bundles cleared net spread thresholds for this run.');
520
+ }
521
+
360
522
  const payload = {
361
523
  action: 'scan',
362
524
  indexerUrl,
@@ -367,11 +529,14 @@ function createRunArbCommand(deps) {
367
529
  markets: options.markets,
368
530
  minNetSpreadPct: options.minNetSpreadPct,
369
531
  feePctPerLeg: options.feePctPerLeg,
532
+ slippagePctPerLeg: options.slippagePctPerLeg,
370
533
  amountUsdc: options.amountUsdc,
534
+ combinatorial: options.combinatorial,
535
+ maxBundleSize: options.maxBundleSize,
371
536
  },
372
- opportunities: snapshots.flatMap((row) => row.opportunities),
373
- snapshots,
374
- diagnostics: [],
537
+ opportunities: iterationSnapshots.flatMap((row) => row.opportunities),
538
+ snapshots: iterationSnapshots,
539
+ diagnostics,
375
540
  };
376
541
 
377
542
  if (context.outputMode === 'json') {
@@ -387,6 +552,7 @@ function createRunArbCommand(deps) {
387
552
  module.exports = {
388
553
  ARB_USAGE,
389
554
  buildArbOpportunities,
555
+ buildCombinatorialArbOpportunities,
390
556
  createRunArbCommand,
391
557
  parseArbScanFlags,
392
558
  };
@@ -410,8 +410,147 @@ function summarizeGroup(group, options, acceptedPairChecks) {
410
410
  };
411
411
  }
412
412
 
413
+ function enumerateCombinations(items, size, onCombination) {
414
+ if (!Array.isArray(items) || !Number.isInteger(size) || size < 1 || size > items.length) return;
415
+ if (typeof onCombination !== 'function') return;
416
+
417
+ const selected = [];
418
+ function walk(startIndex) {
419
+ if (selected.length === size) {
420
+ onCombination(selected.slice());
421
+ return;
422
+ }
423
+ const remaining = size - selected.length;
424
+ const maxStart = items.length - remaining;
425
+ for (let index = startIndex; index <= maxStart; index += 1) {
426
+ selected.push(items[index]);
427
+ walk(index + 1);
428
+ selected.pop();
429
+ }
430
+ }
431
+ walk(0);
432
+ }
433
+
434
+ function resolveCombinatorialSettings(options) {
435
+ return {
436
+ enabled: Boolean(options && options.combinatorial),
437
+ minNetEdgePct: Number.isFinite(options && options.minSpreadPct) ? Number(options.minSpreadPct) : 0,
438
+ feePctPerLeg: Number.isFinite(options && options.combinatorialFeePctPerLeg)
439
+ ? Number(options.combinatorialFeePctPerLeg)
440
+ : Number.isFinite(options && options.feePctPerLeg)
441
+ ? Number(options.feePctPerLeg)
442
+ : 0,
443
+ slippagePctPerLeg: Number.isFinite(options && options.combinatorialSlippagePctPerLeg)
444
+ ? Number(options.combinatorialSlippagePctPerLeg)
445
+ : Number.isFinite(options && options.slippagePctPerLeg)
446
+ ? Number(options.slippagePctPerLeg)
447
+ : 0,
448
+ amountUsdc: Number.isFinite(options && options.combinatorialAmountUsdc)
449
+ ? Number(options.combinatorialAmountUsdc)
450
+ : Number.isFinite(options && options.amountUsdc)
451
+ ? Number(options.amountUsdc)
452
+ : 100,
453
+ maxBundleSize: Number.isInteger(options && options.maxBundleSize) ? Number(options.maxBundleSize) : 4,
454
+ };
455
+ }
456
+
457
+ function buildCombinatorialBundleOpportunities(group, summary, options) {
458
+ const settings = resolveCombinatorialSettings(options);
459
+ if (!settings.enabled) return [];
460
+
461
+ const legs = Array.isArray(group)
462
+ ? group.filter(
463
+ (leg) => leg && Number.isFinite(toNumber(leg.yesPct)) && Number.isFinite(toNumber(leg.noPct)),
464
+ )
465
+ : [];
466
+ if (legs.length < 3) return [];
467
+
468
+ const maxBundleSize = Math.max(3, Math.min(settings.maxBundleSize, legs.length));
469
+ const opportunities = [];
470
+
471
+ for (let bundleSize = 3; bundleSize <= maxBundleSize; bundleSize += 1) {
472
+ enumerateCombinations(legs, bundleSize, (bundle) => {
473
+ const bundleMarketIds = bundle.map((leg) => leg.marketId);
474
+ const bundleVenues = Array.from(new Set(bundle.map((leg) => leg.venue))).sort();
475
+ const bundleCloseValues = bundle.map((leg) => toNumber(leg.closeTimestamp)).filter((value) => value !== null);
476
+ const sumYesPct = round(bundle.reduce((total, leg) => total + Number(leg.yesPct), 0), 6);
477
+ const sumNoPct = round(bundle.reduce((total, leg) => total + Number(leg.noPct), 0), 6);
478
+ const feeImpactPct = round(bundleSize * settings.feePctPerLeg, 6);
479
+ const slippageImpactPct = round(bundleSize * settings.slippagePctPerLeg, 6);
480
+
481
+ const evaluate = (strategy) => {
482
+ const grossEdgePct = strategy === 'buy_yes_bundle' ? round(100 - sumYesPct, 6) : round(sumYesPct - 100, 6);
483
+ if (grossEdgePct <= 0) return;
484
+
485
+ const netEdgePct = round(grossEdgePct - feeImpactPct - slippageImpactPct, 6);
486
+ if (netEdgePct <= 0 || netEdgePct < settings.minNetEdgePct) return;
487
+
488
+ const payoutPct = strategy === 'buy_yes_bundle' ? 100 : round((bundleSize - 1) * 100, 6);
489
+ const totalEntryPct = strategy === 'buy_yes_bundle' ? sumYesPct : sumNoPct;
490
+ const grossProfitUsdc = round((settings.amountUsdc * grossEdgePct) / 100, 6);
491
+ const profitUsdc = round((settings.amountUsdc * netEdgePct) / 100, 6);
492
+
493
+ opportunities.push({
494
+ opportunityType: 'combinatorial',
495
+ strategy,
496
+ groupId: summary.groupId,
497
+ normalizedQuestion: summary.normalizedQuestion,
498
+ bundleSize,
499
+ bundleMarketIds,
500
+ bundleVenues,
501
+ closeTimeWindow: {
502
+ min: bundleCloseValues.length ? Math.min(...bundleCloseValues) : null,
503
+ max: bundleCloseValues.length ? Math.max(...bundleCloseValues) : null,
504
+ },
505
+ sumYesPct,
506
+ sumNoPct,
507
+ totalEntryPct,
508
+ payoutPct,
509
+ grossEdgePct,
510
+ feePctPerLeg: round(settings.feePctPerLeg, 6),
511
+ slippagePctPerLeg: round(settings.slippagePctPerLeg, 6),
512
+ feeImpactPct,
513
+ slippageImpactPct,
514
+ netEdgePct,
515
+ netEdge: round(netEdgePct / 100, 8),
516
+ amountUsdc: round(settings.amountUsdc, 6),
517
+ grossProfitUsdc,
518
+ profitUsdc,
519
+ legs: bundle.map((leg) => ({
520
+ venue: leg.venue,
521
+ marketId: leg.marketId,
522
+ yesPct: leg.yesPct,
523
+ noPct: leg.noPct,
524
+ liquidityUsd: leg.liquidityUsd,
525
+ volumeUsd: leg.volumeUsd,
526
+ closeTimestamp: leg.closeTimestamp,
527
+ rules: options.withRules ? leg.rules || null : undefined,
528
+ sources: options.withRules ? (Array.isArray(leg.sources) ? leg.sources : []) : undefined,
529
+ })),
530
+ });
531
+ };
532
+
533
+ evaluate('buy_yes_bundle');
534
+ evaluate('buy_no_bundle');
535
+ });
536
+ }
537
+
538
+ opportunities.sort((left, right) => {
539
+ if (right.netEdgePct !== left.netEdgePct) {
540
+ return right.netEdgePct - left.netEdgePct;
541
+ }
542
+ if (right.bundleSize !== left.bundleSize) {
543
+ return right.bundleSize - left.bundleSize;
544
+ }
545
+ return String(left.groupId || '').localeCompare(String(right.groupId || ''));
546
+ });
547
+
548
+ return opportunities;
549
+ }
550
+
413
551
  async function scanArbitrage(options) {
414
552
  const venues = Array.from(new Set((options.venues || ['pandora', 'polymarket']).map((value) => String(value).toLowerCase())));
553
+ const combinatorialSettings = resolveCombinatorialSettings(options);
415
554
 
416
555
  const sources = {};
417
556
  const allLegs = [];
@@ -492,18 +631,35 @@ async function scanArbitrage(options) {
492
631
  withRules: options.withRules,
493
632
  includeSimilarity: options.includeSimilarity,
494
633
  questionContains: options.questionContains,
634
+ combinatorial: combinatorialSettings.enabled,
635
+ maxBundleSize: combinatorialSettings.maxBundleSize,
636
+ combinatorialFeePctPerLeg: combinatorialSettings.feePctPerLeg,
637
+ combinatorialSlippagePctPerLeg: combinatorialSettings.slippagePctPerLeg,
638
+ combinatorialAmountUsdc: combinatorialSettings.amountUsdc,
495
639
  },
496
640
  sources,
497
641
  diagnostics,
498
642
  count: 0,
499
643
  opportunities: [],
644
+ ...(combinatorialSettings.enabled
645
+ ? {
646
+ bundleCount: 0,
647
+ bundleOpportunities: [],
648
+ }
649
+ : {}),
500
650
  };
501
651
  }
502
652
 
503
653
  const grouped = buildGroups(allLegs, options);
504
- const opportunities = grouped.groups
505
- .map((group) => summarizeGroup(group, options, grouped.acceptedPairChecks))
506
- .filter(Boolean)
654
+ const groupSummaries = grouped.groups
655
+ .map((group) => ({
656
+ group,
657
+ summary: summarizeGroup(group, options, grouped.acceptedPairChecks),
658
+ }))
659
+ .filter((entry) => Boolean(entry.summary));
660
+
661
+ const opportunities = groupSummaries
662
+ .map((entry) => entry.summary)
507
663
  .sort((a, b) => {
508
664
  const left = Math.max(a.spreadYesPct || 0, a.spreadNoPct || 0);
509
665
  const right = Math.max(b.spreadYesPct || 0, b.spreadNoPct || 0);
@@ -511,6 +667,22 @@ async function scanArbitrage(options) {
511
667
  })
512
668
  .slice(0, options.limit);
513
669
 
670
+ const bundleOpportunities = combinatorialSettings.enabled
671
+ ? groupSummaries
672
+ .flatMap((entry) => buildCombinatorialBundleOpportunities(entry.group, entry.summary, options))
673
+ .sort((a, b) => {
674
+ if ((b.netEdgePct || 0) !== (a.netEdgePct || 0)) {
675
+ return (b.netEdgePct || 0) - (a.netEdgePct || 0);
676
+ }
677
+ return String(a.groupId || '').localeCompare(String(b.groupId || ''));
678
+ })
679
+ .slice(0, options.limit)
680
+ : [];
681
+
682
+ if (combinatorialSettings.enabled && bundleOpportunities.length === 0) {
683
+ diagnostics.push('Combinatorial mode enabled but no bundle opportunities cleared net edge thresholds.');
684
+ }
685
+
514
686
  return {
515
687
  schemaVersion: ARBITRAGE_SCHEMA_VERSION,
516
688
  generatedAt: new Date().toISOString(),
@@ -526,16 +698,28 @@ async function scanArbitrage(options) {
526
698
  withRules: options.withRules,
527
699
  includeSimilarity: options.includeSimilarity,
528
700
  questionContains: options.questionContains,
701
+ combinatorial: combinatorialSettings.enabled,
702
+ maxBundleSize: combinatorialSettings.maxBundleSize,
703
+ combinatorialFeePctPerLeg: combinatorialSettings.feePctPerLeg,
704
+ combinatorialSlippagePctPerLeg: combinatorialSettings.slippagePctPerLeg,
705
+ combinatorialAmountUsdc: combinatorialSettings.amountUsdc,
529
706
  },
530
707
  sources,
531
708
  diagnostics,
532
709
  count: opportunities.length,
533
710
  opportunities,
711
+ ...(combinatorialSettings.enabled
712
+ ? {
713
+ bundleCount: bundleOpportunities.length,
714
+ bundleOpportunities,
715
+ }
716
+ : {}),
534
717
  };
535
718
  }
536
719
 
537
720
  module.exports = {
538
721
  ARBITRAGE_SCHEMA_VERSION,
722
+ buildCombinatorialBundleOpportunities,
539
723
  normalizeQuestion,
540
724
  questionSimilarity,
541
725
  scanArbitrage,