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.
- package/README.md +19 -0
- package/README_FOR_SHARING.md +21 -0
- package/SKILL.md +27 -4
- package/cli/lib/arb_command_service.cjs +175 -9
- package/cli/lib/arbitrage_service.cjs +187 -3
- package/cli/lib/brier_score_service.cjs +271 -0
- package/cli/lib/command_router.cjs +6 -0
- package/cli/lib/error_recovery_service.cjs +85 -0
- package/cli/lib/forecast_store.cjs +361 -0
- package/cli/lib/mcp_tool_registry.cjs +52 -1
- package/cli/lib/mirror_command_service.cjs +3 -3
- package/cli/lib/mirror_econ_service.cjs +409 -1
- package/cli/lib/mirror_handlers/deploy.cjs +2 -2
- package/cli/lib/mirror_handlers/simulate.cjs +1 -1
- package/cli/lib/model_command_service.cjs +99 -0
- package/cli/lib/model_diagnose_service.cjs +204 -0
- package/cli/lib/model_handlers/calibrate.cjs +233 -0
- package/cli/lib/model_handlers/correlation.cjs +458 -0
- package/cli/lib/model_handlers/diagnose.cjs +47 -0
- package/cli/lib/model_handlers/score_brier.cjs +119 -0
- package/cli/lib/model_store.cjs +292 -0
- package/cli/lib/pandora_deploy_service.cjs +4 -2
- package/cli/lib/parsers/mirror_deploy_flags.cjs +7 -2
- package/cli/lib/parsers/mirror_go_flags.cjs +7 -2
- package/cli/lib/parsers/mirror_hedge_calc_flags.cjs +7 -2
- package/cli/lib/parsers/mirror_remaining_flags.cjs +64 -2
- package/cli/lib/parsers/model_flags.cjs +446 -0
- package/cli/lib/parsers/simulate_flags.cjs +310 -0
- package/cli/lib/parsers/watch_flags.cjs +64 -0
- package/cli/lib/quant/abm_market.cjs +282 -0
- package/cli/lib/quant/copula.cjs +407 -0
- package/cli/lib/quant/importance_sampling.cjs +224 -0
- package/cli/lib/quant/mc_stats.cjs +186 -0
- package/cli/lib/quant/particle_filter.cjs +305 -0
- package/cli/lib/quant/rng.cjs +214 -0
- package/cli/lib/quant/variance_reduction.cjs +156 -0
- package/cli/lib/schema_command_service.cjs +138 -2
- package/cli/lib/shared/constants.cjs +6 -0
- package/cli/lib/simulate_command_service.cjs +140 -0
- package/cli/lib/simulate_handlers/agents.cjs +194 -0
- package/cli/lib/simulate_handlers/common.cjs +361 -0
- package/cli/lib/simulate_handlers/mc.cjs +212 -0
- package/cli/lib/simulate_handlers/particle_filter.cjs +370 -0
- package/cli/lib/watch_command_service.cjs +114 -0
- package/cli/pandora.cjs +120 -3
- package/package.json +2 -2
- package/references/creation-script.md +1 -1
- package/tests/cli/cli.integration.test.cjs +464 -2
- package/tests/cli/mcp.integration.test.cjs +100 -0
- package/tests/unit/abm_market.test.cjs +226 -0
- package/tests/unit/brier_scoring.test.cjs +74 -0
- package/tests/unit/brier_watch_command.test.cjs +121 -0
- package/tests/unit/combinatorial_arb.test.cjs +171 -0
- package/tests/unit/mirror_simulate_mc.test.cjs +152 -0
- package/tests/unit/model_calibrate.test.cjs +142 -0
- package/tests/unit/model_correlation.test.cjs +147 -0
- package/tests/unit/model_diagnose.test.cjs +83 -0
- package/tests/unit/quant_core.test.cjs +104 -0
- package/tests/unit/quant_models.test.cjs +85 -0
- package/tests/unit/quant_stores.test.cjs +134 -0
- package/tests/unit/simulate_command_service.test.cjs +91 -0
- package/tests/unit/simulate_flags.test.cjs +185 -0
- 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)
|
package/README_FOR_SHARING.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
139
|
-
go --polymarket-market-id <id>|--polymarket-slug <slug> [--liquidity-usdc <n>] [--fee-tier 500
|
|
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
|
|
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
|
|
325
|
-
const
|
|
326
|
-
marketSnapshots
|
|
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
|
-
|
|
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:
|
|
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
|
|
505
|
-
.map((group) =>
|
|
506
|
-
|
|
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,
|