pandora-cli-skills 1.1.47 → 1.1.48
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 +1 -0
- package/README_FOR_SHARING.md +2 -1
- package/SKILL.md +4 -3
- package/cli/lib/mirror_command_service.cjs +1 -1
- package/cli/lib/mirror_handlers/browse.cjs +2 -2
- package/cli/lib/mirror_service.cjs +1 -0
- package/cli/lib/parsers/mirror_remaining_flags.cjs +29 -0
- package/cli/lib/polymarket_trade_adapter.cjs +167 -12
- package/cli/lib/schema_command_service.cjs +20 -0
- package/package.json +1 -1
- package/tests/cli/cli.integration.test.cjs +99 -0
- package/tests/unit/new-features.test.cjs +118 -0
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
|
|
|
122
122
|
- `pandora odds record|history`
|
|
123
123
|
- `pandora autopilot run|once`
|
|
124
124
|
- `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close`
|
|
125
|
+
- `mirror browse` supports `--polymarket-tag-id|--polymarket-tag-ids` (aliases `--sport-tag-id|--sport-tag-ids`) for sports-tagged Gamma event discovery.
|
|
125
126
|
- `pandora polymarket check|approve|preflight|trade`
|
|
126
127
|
- `pandora resolve`
|
|
127
128
|
- `pandora lp add|remove|positions`
|
package/README_FOR_SHARING.md
CHANGED
|
@@ -200,6 +200,7 @@ Mirror advanced flags (for operator tuning):
|
|
|
200
200
|
- `--sync-interval-ms <ms>` on `mirror go` to control auto-sync tick cadence.
|
|
201
201
|
- `--oracle <address>` / `--factory <address>` on `mirror deploy` and `mirror go` for explicit contract overrides.
|
|
202
202
|
- `--polymarket-gamma-mock-url <url>` on `mirror browse|plan|verify|go|sync|status` for deterministic mock-source testing.
|
|
203
|
+
- `--polymarket-tag-id <id>` / `--polymarket-tag-ids <csv>` on `mirror browse` (aliases: `--sport-tag-id`, `--sport-tag-ids`) to query sports-tagged Gamma events.
|
|
203
204
|
- `--no-stream` on `mirror sync` to disable per-tick stdout line streaming in run mode.
|
|
204
205
|
- `--pid-file <path>` on `mirror sync stop|status` for explicit daemon process selection.
|
|
205
206
|
|
|
@@ -219,7 +220,7 @@ Mirror advanced flags (for operator tuning):
|
|
|
219
220
|
- `pandora export --wallet <0x...> --format csv --out ./trades.csv`
|
|
220
221
|
- `pandora arbitrage --venues pandora,polymarket --min-spread-pct 2 --cross-venue-only --with-rules --include-similarity`
|
|
221
222
|
- `pandora autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper`
|
|
222
|
-
- `pandora mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
|
|
223
|
+
- `pandora mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
|
|
223
224
|
- `pandora mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity`
|
|
224
225
|
- `pandora mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper`
|
|
225
226
|
- `pandora mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity`
|
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.48
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Pandora CLI & Skills
|
|
@@ -317,7 +317,7 @@ pandora --output json history --wallet <0x...> --limit 50
|
|
|
317
317
|
pandora --output json export --wallet <0x...> --format csv --out ./trades.csv
|
|
318
318
|
pandora --output json arbitrage --venues pandora,polymarket --min-spread-pct 3 --cross-venue-only --with-rules --include-similarity
|
|
319
319
|
pandora --output json autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper
|
|
320
|
-
pandora --output json mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
|
|
320
|
+
pandora --output json mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
|
|
321
321
|
pandora --output json mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity
|
|
322
322
|
pandora --output json mirror lp-explain --liquidity-usdc 10000 --source-yes-pct 58
|
|
323
323
|
pandora --output json mirror hedge-calc --reserve-yes-usdc 8 --reserve-no-usdc 12 --excess-no-usdc 2 --polymarket-yes-pct 60
|
|
@@ -562,7 +562,8 @@ Error envelope:
|
|
|
562
562
|
- `{ ok: true, command: "autopilot", data: { schemaVersion, generatedAt, strategyHash, mode, executeLive, stateFile, killSwitchFile, iterationsRequested, iterationsCompleted, stoppedReason?, parameters: { marketAddress, side, amountUsdc, triggerYesBelow?, triggerYesAbove?, intervalMs, cooldownMs, maxAmountUsdc?, maxOpenExposureUsdc, dailySpendCapUsdc, maxTradesPerDay }, state, actionCount, actions[], snapshots[], webhookReports[] } }`
|
|
563
563
|
- `mirror browse`:
|
|
564
564
|
- `{ ok: true, command: "mirror.browse", data: { schemaVersion, generatedAt, source, gammaApiError, filters, count, items[], diagnostics[] } }`
|
|
565
|
-
-
|
|
565
|
+
- `filters` can include `polymarketTagIds[]` when using `--polymarket-tag-id|--polymarket-tag-ids` (or `--sport-tag-id|--sport-tag-ids` aliases).
|
|
566
|
+
- each candidate row can include `eventId`, `eventSlug`, `eventTitle`, and `existingMirror: { marketAddress, similarity } | null`.
|
|
566
567
|
- `mirror go`:
|
|
567
568
|
- `{ ok: true, command: "mirror.go", data: { schemaVersion, generatedAt, mode, plan, deploy, verify, sync, polymarketPreflight, suggestedSyncCommand, trustManifest, diagnostics[] } }`
|
|
568
569
|
- `plan` is the same payload shape as `mirror.plan`; `deploy` is the same payload shape as `mirror.deploy`; `sync` is null unless `--auto-sync` is used.
|
|
@@ -91,7 +91,7 @@ function createRunMirrorCommand(deps) {
|
|
|
91
91
|
console.log('');
|
|
92
92
|
console.log('Subcommands:');
|
|
93
93
|
console.log(
|
|
94
|
-
' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
94
|
+
' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
95
95
|
);
|
|
96
96
|
console.log(
|
|
97
97
|
' plan --source polymarket --polymarket-market-id <id>|--polymarket-slug <slug> [--chain-id <id>] [--target-slippage-bps <n>] [--turnover-target <n>] [--depth-slippage-bps <n>] [--safety-multiplier <n>] [--min-liquidity-usdc <n>] [--max-liquidity-usdc <n>] [--with-rules] [--include-similarity] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
@@ -22,12 +22,12 @@ module.exports = async function handleMirrorBrowse({ shared, context, deps }) {
|
|
|
22
22
|
context.outputMode,
|
|
23
23
|
'mirror.browse.help',
|
|
24
24
|
commandHelpPayload(
|
|
25
|
-
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
|
|
25
|
+
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
26
26
|
),
|
|
27
27
|
);
|
|
28
28
|
} else {
|
|
29
29
|
console.log(
|
|
30
|
-
'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
|
|
30
|
+
'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
return;
|
|
@@ -395,6 +395,7 @@ async function browseMirrorMarkets(options = {}) {
|
|
|
395
395
|
gammaUrl: options.polymarketGammaUrl,
|
|
396
396
|
gammaMockUrl: options.polymarketGammaMockUrl,
|
|
397
397
|
mockUrl: options.polymarketMockUrl,
|
|
398
|
+
polymarketTagIds: Array.isArray(options.polymarketTagIds) ? options.polymarketTagIds : [],
|
|
398
399
|
timeoutMs: options.timeoutMs,
|
|
399
400
|
minYesPct: options.minYesPct,
|
|
400
401
|
maxYesPct: options.maxYesPct,
|
|
@@ -39,8 +39,13 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
39
39
|
polymarketGammaUrl: null,
|
|
40
40
|
polymarketGammaMockUrl: null,
|
|
41
41
|
polymarketMockUrl: null,
|
|
42
|
+
polymarketTagIds: [],
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
function pushTagId(rawValue, flagName) {
|
|
46
|
+
options.polymarketTagIds.push(parsePositiveInteger(rawValue, flagName));
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
for (let i = 0; i < args.length; i += 1) {
|
|
45
50
|
const token = args[i];
|
|
46
51
|
if (token === '--min-yes-pct') {
|
|
@@ -98,6 +103,26 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
98
103
|
i += 1;
|
|
99
104
|
continue;
|
|
100
105
|
}
|
|
106
|
+
if (token === '--polymarket-tag-id' || token === '--sport-tag-id') {
|
|
107
|
+
pushTagId(requireFlagValue(args, i, token), token);
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (token === '--polymarket-tag-ids' || token === '--sport-tag-ids') {
|
|
112
|
+
const raw = requireFlagValue(args, i, token);
|
|
113
|
+
const values = String(raw)
|
|
114
|
+
.split(',')
|
|
115
|
+
.map((value) => value.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (!values.length) {
|
|
118
|
+
throw new CliError('INVALID_FLAG_VALUE', `${token} must include at least one positive integer tag id.`);
|
|
119
|
+
}
|
|
120
|
+
for (const value of values) {
|
|
121
|
+
pushTagId(value, token);
|
|
122
|
+
}
|
|
123
|
+
i += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
101
126
|
throw new CliError('UNKNOWN_FLAG', `Unknown flag for mirror browse: ${token}`);
|
|
102
127
|
}
|
|
103
128
|
|
|
@@ -105,6 +130,10 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
105
130
|
throw new CliError('INVALID_ARGS', '--min-yes-pct cannot be greater than --max-yes-pct.');
|
|
106
131
|
}
|
|
107
132
|
|
|
133
|
+
if (options.polymarketTagIds.length) {
|
|
134
|
+
options.polymarketTagIds = Array.from(new Set(options.polymarketTagIds));
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
return options;
|
|
109
138
|
};
|
|
110
139
|
}
|
|
@@ -405,6 +405,9 @@ function normalizeMarketRow(row) {
|
|
|
405
405
|
).trim() || null,
|
|
406
406
|
slug: String((row && (row.market_slug || row.marketSlug || row.slug)) || '').trim() || null,
|
|
407
407
|
question: extractQuestionText(row),
|
|
408
|
+
eventId: toStringOrNull(row && (row.event_id || row.eventId)),
|
|
409
|
+
eventSlug: toStringOrNull(row && (row.event_slug || row.eventSlug)),
|
|
410
|
+
eventTitle: toStringOrNull(row && (row.event_title || row.eventTitle)),
|
|
408
411
|
description: rulesSections.length ? rulesSections.join('\n\n') : null,
|
|
409
412
|
closeTimestamp: toTimestampSeconds(
|
|
410
413
|
row &&
|
|
@@ -479,6 +482,14 @@ function parseMarketsPayload(payload) {
|
|
|
479
482
|
return [];
|
|
480
483
|
}
|
|
481
484
|
|
|
485
|
+
function parseEventsPayload(payload) {
|
|
486
|
+
if (Array.isArray(payload)) return payload;
|
|
487
|
+
if (payload && Array.isArray(payload.events)) return payload.events;
|
|
488
|
+
if (payload && payload.data && Array.isArray(payload.data.events)) return payload.data.events;
|
|
489
|
+
if (payload && Array.isArray(payload.data)) return payload.data;
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
|
|
482
493
|
async function fetchJson(url, timeoutMs) {
|
|
483
494
|
const controller = new AbortController();
|
|
484
495
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -530,6 +541,15 @@ function buildGammaUrl(baseUrl, params) {
|
|
|
530
541
|
return url.toString();
|
|
531
542
|
}
|
|
532
543
|
|
|
544
|
+
function buildGammaEventsUrl(baseUrl, params) {
|
|
545
|
+
const url = new URL(`${baseUrl}/events`);
|
|
546
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
547
|
+
if (value === null || value === undefined || value === '') continue;
|
|
548
|
+
url.searchParams.set(key, String(value));
|
|
549
|
+
}
|
|
550
|
+
return url.toString();
|
|
551
|
+
}
|
|
552
|
+
|
|
533
553
|
async function fetchGammaRows(params, options = {}, diagnostics = []) {
|
|
534
554
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
535
555
|
const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
|
|
@@ -543,6 +563,120 @@ async function fetchGammaRows(params, options = {}, diagnostics = []) {
|
|
|
543
563
|
}
|
|
544
564
|
}
|
|
545
565
|
|
|
566
|
+
function makeMarketDedupeKey(row) {
|
|
567
|
+
return (
|
|
568
|
+
normalizeText(
|
|
569
|
+
row &&
|
|
570
|
+
(row.condition_id ||
|
|
571
|
+
row.conditionId ||
|
|
572
|
+
row.question_id ||
|
|
573
|
+
row.questionId ||
|
|
574
|
+
row.market_id ||
|
|
575
|
+
row.marketId ||
|
|
576
|
+
row.id ||
|
|
577
|
+
row.slug),
|
|
578
|
+
) || null
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function flattenEventMarkets(events) {
|
|
583
|
+
const output = [];
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
|
|
586
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
587
|
+
const markets = Array.isArray(event && event.markets) ? event.markets : [];
|
|
588
|
+
for (const market of markets) {
|
|
589
|
+
if (!market || typeof market !== 'object') continue;
|
|
590
|
+
const dedupeKey = makeMarketDedupeKey(market);
|
|
591
|
+
if (dedupeKey && seen.has(dedupeKey)) continue;
|
|
592
|
+
if (dedupeKey) seen.add(dedupeKey);
|
|
593
|
+
output.push({
|
|
594
|
+
...market,
|
|
595
|
+
event_id: event && event.id !== undefined ? event.id : null,
|
|
596
|
+
event_slug: event && event.slug ? event.slug : null,
|
|
597
|
+
event_title: event && event.title ? event.title : null,
|
|
598
|
+
event_tags: Array.isArray(event && event.tags) ? event.tags : [],
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return output;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function normalizeTagIdList(input) {
|
|
607
|
+
const values = Array.isArray(input) ? input : [];
|
|
608
|
+
const normalized = [];
|
|
609
|
+
for (const value of values) {
|
|
610
|
+
const numeric = Number(value);
|
|
611
|
+
if (!Number.isFinite(numeric)) continue;
|
|
612
|
+
const asInt = Math.trunc(numeric);
|
|
613
|
+
if (asInt <= 0) continue;
|
|
614
|
+
normalized.push(asInt);
|
|
615
|
+
}
|
|
616
|
+
return Array.from(new Set(normalized));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function fetchGammaRowsByTagIds(params, options = {}, diagnostics = []) {
|
|
620
|
+
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
621
|
+
const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
|
|
622
|
+
const tagIds = normalizeTagIdList(params && params.tagIds);
|
|
623
|
+
if (!tagIds.length) return [];
|
|
624
|
+
|
|
625
|
+
if (options.gammaMockUrl) {
|
|
626
|
+
try {
|
|
627
|
+
const payload = await fetchJson(options.gammaMockUrl, timeoutMs);
|
|
628
|
+
const events = parseEventsPayload(payload);
|
|
629
|
+
return flattenEventMarkets(events);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
diagnostics.push(`Gamma sports-events request failed (${options.gammaMockUrl}): ${formatNetworkError(err)}`);
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const allRows = [];
|
|
637
|
+
const perTagResults = await Promise.all(
|
|
638
|
+
tagIds.map(async (tagId) => {
|
|
639
|
+
const queryParams = {
|
|
640
|
+
...(params || {}),
|
|
641
|
+
tag_id: tagId,
|
|
642
|
+
};
|
|
643
|
+
delete queryParams.tagIds;
|
|
644
|
+
const targetUrl = buildGammaEventsUrl(gammaUrl, queryParams);
|
|
645
|
+
try {
|
|
646
|
+
const payload = await fetchJson(targetUrl, timeoutMs);
|
|
647
|
+
const events = parseEventsPayload(payload);
|
|
648
|
+
return {
|
|
649
|
+
rows: flattenEventMarkets(events),
|
|
650
|
+
error: null,
|
|
651
|
+
};
|
|
652
|
+
} catch (err) {
|
|
653
|
+
return {
|
|
654
|
+
rows: [],
|
|
655
|
+
error: `Gamma sports-events request failed (${targetUrl}): ${formatNetworkError(err)}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
for (const result of perTagResults) {
|
|
662
|
+
if (result.error) {
|
|
663
|
+
diagnostics.push(result.error);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
allRows.push(...result.rows);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const deduped = [];
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
for (const row of allRows) {
|
|
672
|
+
const dedupeKey = makeMarketDedupeKey(row);
|
|
673
|
+
if (dedupeKey && seen.has(dedupeKey)) continue;
|
|
674
|
+
if (dedupeKey) seen.add(dedupeKey);
|
|
675
|
+
deduped.push(row);
|
|
676
|
+
}
|
|
677
|
+
return deduped;
|
|
678
|
+
}
|
|
679
|
+
|
|
546
680
|
function extractConditionId(row) {
|
|
547
681
|
const value = toStringOrNull(
|
|
548
682
|
row &&
|
|
@@ -998,21 +1132,38 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
998
1132
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
999
1133
|
const requestedLimit = Number.isInteger(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 10;
|
|
1000
1134
|
const scanLimit = Math.max(requestedLimit * 5, 100);
|
|
1135
|
+
const polymarketTagIds = normalizeTagIdList(options.polymarketTagIds);
|
|
1136
|
+
const useSportsEventsEndpoint = polymarketTagIds.length > 0;
|
|
1001
1137
|
|
|
1002
1138
|
let rows = [];
|
|
1139
|
+
let sourceType = options.mockUrl ? 'polymarket:mock' : 'polymarket:gamma';
|
|
1003
1140
|
if (options.mockUrl) {
|
|
1004
1141
|
const payload = await fetchMockPayload(options.mockUrl, timeoutMs);
|
|
1005
1142
|
rows = parseMarketsPayload(payload);
|
|
1006
1143
|
} else {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1144
|
+
if (useSportsEventsEndpoint) {
|
|
1145
|
+
rows = await fetchGammaRowsByTagIds(
|
|
1146
|
+
{
|
|
1147
|
+
tagIds: polymarketTagIds,
|
|
1148
|
+
active: true,
|
|
1149
|
+
closed: false,
|
|
1150
|
+
limit: Math.min(scanLimit, 500),
|
|
1151
|
+
},
|
|
1152
|
+
options,
|
|
1153
|
+
diagnostics,
|
|
1154
|
+
);
|
|
1155
|
+
sourceType = 'polymarket:gamma-events';
|
|
1156
|
+
} else {
|
|
1157
|
+
rows = await fetchGammaRows(
|
|
1158
|
+
{
|
|
1159
|
+
active: true,
|
|
1160
|
+
closed: false,
|
|
1161
|
+
limit: Math.min(scanLimit, 500),
|
|
1162
|
+
},
|
|
1163
|
+
options,
|
|
1164
|
+
diagnostics,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1016
1167
|
}
|
|
1017
1168
|
|
|
1018
1169
|
const minYesPct = toOptionalNumber(options.minYesPct);
|
|
@@ -1039,6 +1190,9 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1039
1190
|
const items = filtered.slice(0, requestedLimit).map((item) => ({
|
|
1040
1191
|
marketId: item.marketId,
|
|
1041
1192
|
slug: item.slug,
|
|
1193
|
+
eventId: item.eventId,
|
|
1194
|
+
eventSlug: item.eventSlug,
|
|
1195
|
+
eventTitle: item.eventTitle,
|
|
1042
1196
|
question: item.question,
|
|
1043
1197
|
closeTimestamp: item.closeTimestamp,
|
|
1044
1198
|
yesPct: item.yesPct,
|
|
@@ -1048,16 +1202,16 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1048
1202
|
active: item.active,
|
|
1049
1203
|
resolved: item.resolved,
|
|
1050
1204
|
url: item.url,
|
|
1051
|
-
sourceType: item.source ||
|
|
1205
|
+
sourceType: item.source || sourceType,
|
|
1052
1206
|
}));
|
|
1053
1207
|
|
|
1054
1208
|
const gammaApiError =
|
|
1055
|
-
diagnostics.find((line) => /^Gamma request failed/i.test(String(line || ''))) || null;
|
|
1209
|
+
diagnostics.find((line) => /^Gamma( sports-events)? request failed/i.test(String(line || ''))) || null;
|
|
1056
1210
|
|
|
1057
1211
|
return {
|
|
1058
1212
|
schemaVersion: '1.0.0',
|
|
1059
1213
|
generatedAt: new Date().toISOString(),
|
|
1060
|
-
source:
|
|
1214
|
+
source: sourceType,
|
|
1061
1215
|
filters: {
|
|
1062
1216
|
minYesPct,
|
|
1063
1217
|
maxYesPct,
|
|
@@ -1066,6 +1220,7 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1066
1220
|
closesBefore,
|
|
1067
1221
|
questionContains: options.questionContains || null,
|
|
1068
1222
|
limit: requestedLimit,
|
|
1223
|
+
polymarketTagIds,
|
|
1069
1224
|
},
|
|
1070
1225
|
count: items.length,
|
|
1071
1226
|
items,
|
|
@@ -264,6 +264,13 @@ function buildCommandDescriptors() {
|
|
|
264
264
|
emits: ['sports.resolve.plan', 'sports.help'],
|
|
265
265
|
dataSchema: '#/definitions/SportsResolvePlanPayload',
|
|
266
266
|
}),
|
|
267
|
+
'mirror.browse': commandDescriptor({
|
|
268
|
+
summary: 'Browse Polymarket mirror candidates with optional sports tag filters.',
|
|
269
|
+
usage:
|
|
270
|
+
'pandora [--output table|json] mirror 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-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
271
|
+
emits: ['mirror.browse', 'mirror.browse.help'],
|
|
272
|
+
dataSchema: '#/definitions/MirrorBrowsePayload',
|
|
273
|
+
}),
|
|
267
274
|
'mirror.plan': commandDescriptor({
|
|
268
275
|
summary: 'Generate mirror sizing/distribution plan from Polymarket source.',
|
|
269
276
|
usage:
|
|
@@ -812,6 +819,19 @@ function buildSchemaPayload() {
|
|
|
812
819
|
generatedAt: { type: 'string', format: 'date-time' },
|
|
813
820
|
},
|
|
814
821
|
},
|
|
822
|
+
MirrorBrowsePayload: {
|
|
823
|
+
type: 'object',
|
|
824
|
+
properties: {
|
|
825
|
+
source: { type: 'string' },
|
|
826
|
+
gammaApiError: { type: ['string', 'null'] },
|
|
827
|
+
filters: { type: 'object' },
|
|
828
|
+
count: { type: 'integer' },
|
|
829
|
+
items: { type: 'array', items: { type: 'object' } },
|
|
830
|
+
diagnostics: { type: 'array', items: { type: 'string' } },
|
|
831
|
+
schemaVersion: { type: 'string' },
|
|
832
|
+
generatedAt: { type: 'string', format: 'date-time' },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
815
835
|
MirrorPlanPayload: {
|
|
816
836
|
type: 'object',
|
|
817
837
|
properties: {
|
package/package.json
CHANGED
|
@@ -1090,6 +1090,9 @@ test('schema command returns envelope schema plus command descriptors', () => {
|
|
|
1090
1090
|
assert.ok(payload.data.commandDescriptors.quote.emits.includes('quote'));
|
|
1091
1091
|
assert.ok(payload.data.commandDescriptors.trade);
|
|
1092
1092
|
assert.equal(payload.data.commandDescriptors.trade.dataSchema, '#/definitions/TradePayload');
|
|
1093
|
+
assert.ok(payload.data.commandDescriptors['mirror.browse']);
|
|
1094
|
+
assert.equal(payload.data.commandDescriptors['mirror.browse'].dataSchema, '#/definitions/MirrorBrowsePayload');
|
|
1095
|
+
assert.match(payload.data.commandDescriptors['mirror.browse'].usage, /--polymarket-tag-id/);
|
|
1093
1096
|
assert.ok(payload.data.commandDescriptors['mirror.plan']);
|
|
1094
1097
|
assert.equal(payload.data.commandDescriptors['mirror.plan'].dataSchema, '#/definitions/MirrorPlanPayload');
|
|
1095
1098
|
assert.ok(payload.data.commandDescriptors['risk.show']);
|
|
@@ -1150,6 +1153,7 @@ test('schema command returns envelope schema plus command descriptors', () => {
|
|
|
1150
1153
|
assert.ok(payload.data.definitions.ModelCorrelationPayload);
|
|
1151
1154
|
assert.ok(payload.data.definitions.ModelDiagnosePayload);
|
|
1152
1155
|
assert.ok(payload.data.definitions.ErrorRecoveryPayload);
|
|
1156
|
+
assert.ok(payload.data.definitions.MirrorBrowsePayload);
|
|
1153
1157
|
});
|
|
1154
1158
|
|
|
1155
1159
|
test('schema command rejects unknown trailing flags', () => {
|
|
@@ -4825,6 +4829,24 @@ test('mirror browse rejects invalid calendar rollover dates', () => {
|
|
|
4825
4829
|
assert.match(payload.error.message, /real calendar date/);
|
|
4826
4830
|
});
|
|
4827
4831
|
|
|
4832
|
+
test('mirror browse rejects invalid tag id values', () => {
|
|
4833
|
+
const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-id', '0']);
|
|
4834
|
+
assert.equal(result.status, 1);
|
|
4835
|
+
const payload = parseJsonOutput(result);
|
|
4836
|
+
assert.equal(payload.ok, false);
|
|
4837
|
+
assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
|
|
4838
|
+
assert.match(payload.error.message, /--polymarket-tag-id must be a positive integer/i);
|
|
4839
|
+
});
|
|
4840
|
+
|
|
4841
|
+
test('mirror browse rejects empty tag-id csv values', () => {
|
|
4842
|
+
const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-ids', ', ,']);
|
|
4843
|
+
assert.equal(result.status, 1);
|
|
4844
|
+
const payload = parseJsonOutput(result);
|
|
4845
|
+
assert.equal(payload.ok, false);
|
|
4846
|
+
assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
|
|
4847
|
+
assert.match(payload.error.message, /must include at least one positive integer tag id/i);
|
|
4848
|
+
});
|
|
4849
|
+
|
|
4828
4850
|
test('boolean flags with --key=false do not silently flip behavior', () => {
|
|
4829
4851
|
const result = runCli(['--output', 'json', 'scan', '--active=false']);
|
|
4830
4852
|
assert.equal(result.status, 1);
|
|
@@ -4924,6 +4946,83 @@ test('mirror browse returns candidate markets with existing mirror hint', async
|
|
|
4924
4946
|
}
|
|
4925
4947
|
});
|
|
4926
4948
|
|
|
4949
|
+
test('mirror browse supports sports tag filters via gamma events endpoint', async () => {
|
|
4950
|
+
const gamma = await startJsonHttpServer((request) => {
|
|
4951
|
+
const parsed = new URL(request.url || '/', 'http://127.0.0.1');
|
|
4952
|
+
if (parsed.pathname !== '/events') {
|
|
4953
|
+
return { status: 404, body: { error: 'not found' } };
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
const tagId = parsed.searchParams.get('tag_id');
|
|
4957
|
+
if (tagId !== '82') {
|
|
4958
|
+
return { body: { events: [] } };
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
return {
|
|
4962
|
+
body: {
|
|
4963
|
+
events: [
|
|
4964
|
+
{
|
|
4965
|
+
id: 'evt-epl-1',
|
|
4966
|
+
slug: 'everton-v-burnley',
|
|
4967
|
+
title: 'Everton vs Burnley',
|
|
4968
|
+
markets: [
|
|
4969
|
+
{
|
|
4970
|
+
condition_id: 'poly-epl-c1',
|
|
4971
|
+
market_slug: 'everton-v-burnley-home',
|
|
4972
|
+
question: 'Will Everton beat Burnley?',
|
|
4973
|
+
end_date_iso: FIXED_MIRROR_CLOSE_ISO,
|
|
4974
|
+
active: true,
|
|
4975
|
+
closed: false,
|
|
4976
|
+
volume24hr: 550000,
|
|
4977
|
+
tokens: [
|
|
4978
|
+
{ outcome: 'Yes', price: '0.605', token_id: 'poly-epl-yes-1' },
|
|
4979
|
+
{ outcome: 'No', price: '0.395', token_id: 'poly-epl-no-1' },
|
|
4980
|
+
],
|
|
4981
|
+
},
|
|
4982
|
+
],
|
|
4983
|
+
},
|
|
4984
|
+
],
|
|
4985
|
+
},
|
|
4986
|
+
};
|
|
4987
|
+
});
|
|
4988
|
+
|
|
4989
|
+
try {
|
|
4990
|
+
const result = await runCliAsync([
|
|
4991
|
+
'--output',
|
|
4992
|
+
'json',
|
|
4993
|
+
'mirror',
|
|
4994
|
+
'browse',
|
|
4995
|
+
'--skip-dotenv',
|
|
4996
|
+
'--polymarket-gamma-url',
|
|
4997
|
+
gamma.url,
|
|
4998
|
+
'--polymarket-tag-id',
|
|
4999
|
+
'82',
|
|
5000
|
+
'--limit',
|
|
5001
|
+
'5',
|
|
5002
|
+
]);
|
|
5003
|
+
|
|
5004
|
+
assert.equal(result.status, 0);
|
|
5005
|
+
const payload = parseJsonOutput(result);
|
|
5006
|
+
assert.equal(payload.ok, true);
|
|
5007
|
+
assert.equal(payload.command, 'mirror.browse');
|
|
5008
|
+
assert.equal(payload.data.source, 'polymarket:gamma-events');
|
|
5009
|
+
assert.deepEqual(payload.data.filters.polymarketTagIds, [82]);
|
|
5010
|
+
assert.equal(payload.data.count, 1);
|
|
5011
|
+
assert.equal(payload.data.items[0].eventSlug, 'everton-v-burnley');
|
|
5012
|
+
assert.equal(payload.data.items[0].eventTitle, 'Everton vs Burnley');
|
|
5013
|
+
assert.equal(payload.data.items[0].eventId, 'evt-epl-1');
|
|
5014
|
+
|
|
5015
|
+
const eventRequest = gamma.requests.find((entry) => String(entry.url || '').startsWith('/events?'));
|
|
5016
|
+
assert.ok(eventRequest);
|
|
5017
|
+
const parsed = new URL(eventRequest.url, 'http://127.0.0.1');
|
|
5018
|
+
assert.equal(parsed.searchParams.get('tag_id'), '82');
|
|
5019
|
+
assert.equal(parsed.searchParams.get('active'), 'true');
|
|
5020
|
+
assert.equal(parsed.searchParams.get('closed'), 'false');
|
|
5021
|
+
} finally {
|
|
5022
|
+
await gamma.close();
|
|
5023
|
+
}
|
|
5024
|
+
});
|
|
5025
|
+
|
|
4927
5026
|
test('mirror sync accepts --market-address with --dry-run mode alias', async () => {
|
|
4928
5027
|
const tempDir = createTempDir('pandora-mirror-sync-aliases-');
|
|
4929
5028
|
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
@@ -1027,6 +1027,124 @@ test('browsePolymarketMarkets filters mock payload deterministically', async ()
|
|
|
1027
1027
|
}
|
|
1028
1028
|
});
|
|
1029
1029
|
|
|
1030
|
+
test('browsePolymarketMarkets uses gamma events endpoint for tag-id sports discovery', async () => {
|
|
1031
|
+
const requests = [];
|
|
1032
|
+
const server = http.createServer((req, res) => {
|
|
1033
|
+
requests.push(req.url || '/');
|
|
1034
|
+
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
1035
|
+
const tagId = parsed.searchParams.get('tag_id');
|
|
1036
|
+
|
|
1037
|
+
res.statusCode = 200;
|
|
1038
|
+
res.setHeader('content-type', 'application/json');
|
|
1039
|
+
|
|
1040
|
+
if (parsed.pathname !== '/events') {
|
|
1041
|
+
res.end(JSON.stringify({ events: [] }));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (tagId === '82') {
|
|
1046
|
+
res.end(
|
|
1047
|
+
JSON.stringify({
|
|
1048
|
+
events: [
|
|
1049
|
+
{
|
|
1050
|
+
id: 'evt-82',
|
|
1051
|
+
slug: 'everton-v-burnley',
|
|
1052
|
+
title: 'Everton vs Burnley',
|
|
1053
|
+
markets: [
|
|
1054
|
+
{
|
|
1055
|
+
condition_id: 'sports-c1',
|
|
1056
|
+
market_slug: 'everton-v-burnley-home',
|
|
1057
|
+
question: 'Will Everton beat Burnley?',
|
|
1058
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1059
|
+
active: true,
|
|
1060
|
+
closed: false,
|
|
1061
|
+
volume24hr: 500000,
|
|
1062
|
+
tokens: [
|
|
1063
|
+
{ outcome: 'Yes', price: '0.605', token_id: 'yes-sports-1' },
|
|
1064
|
+
{ outcome: 'No', price: '0.395', token_id: 'no-sports-1' },
|
|
1065
|
+
],
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
}),
|
|
1071
|
+
);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (tagId === '100350') {
|
|
1076
|
+
res.end(
|
|
1077
|
+
JSON.stringify({
|
|
1078
|
+
events: [
|
|
1079
|
+
{
|
|
1080
|
+
id: 'evt-100350',
|
|
1081
|
+
slug: 'leeds-v-sunderland',
|
|
1082
|
+
title: 'Leeds vs Sunderland',
|
|
1083
|
+
markets: [
|
|
1084
|
+
{
|
|
1085
|
+
// Duplicate condition id should be deduped across tag-id scans.
|
|
1086
|
+
condition_id: 'sports-c1',
|
|
1087
|
+
market_slug: 'duplicate-market-ignored',
|
|
1088
|
+
question: 'Duplicate row should be ignored',
|
|
1089
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1090
|
+
active: true,
|
|
1091
|
+
closed: false,
|
|
1092
|
+
volume24hr: 1,
|
|
1093
|
+
tokens: [
|
|
1094
|
+
{ outcome: 'Yes', price: '0.5', token_id: 'dup-yes' },
|
|
1095
|
+
{ outcome: 'No', price: '0.5', token_id: 'dup-no' },
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
condition_id: 'sports-c2',
|
|
1100
|
+
market_slug: 'leeds-v-sunderland-home',
|
|
1101
|
+
question: 'Will Leeds beat Sunderland?',
|
|
1102
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1103
|
+
active: true,
|
|
1104
|
+
closed: false,
|
|
1105
|
+
volume24hr: 400000,
|
|
1106
|
+
tokens: [
|
|
1107
|
+
{ outcome: 'Yes', price: '0.495', token_id: 'yes-sports-2' },
|
|
1108
|
+
{ outcome: 'No', price: '0.505', token_id: 'no-sports-2' },
|
|
1109
|
+
],
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
],
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
res.end(JSON.stringify({ events: [] }));
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1123
|
+
const { port } = server.address();
|
|
1124
|
+
const gammaUrl = `http://127.0.0.1:${port}`;
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
const payload = await browsePolymarketMarkets({
|
|
1128
|
+
gammaUrl,
|
|
1129
|
+
polymarketTagIds: [82, 100350],
|
|
1130
|
+
limit: 10,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
assert.equal(payload.source, 'polymarket:gamma-events');
|
|
1134
|
+
assert.deepEqual(payload.filters.polymarketTagIds, [82, 100350]);
|
|
1135
|
+
assert.equal(payload.count, 2);
|
|
1136
|
+
assert.equal(payload.items[0].eventSlug, 'everton-v-burnley');
|
|
1137
|
+
assert.equal(payload.items[1].eventSlug, 'leeds-v-sunderland');
|
|
1138
|
+
assert.equal(payload.items[0].eventTitle, 'Everton vs Burnley');
|
|
1139
|
+
assert.equal(payload.items[0].eventId, 'evt-82');
|
|
1140
|
+
|
|
1141
|
+
const eventRequests = requests.filter((entry) => String(entry).startsWith('/events?'));
|
|
1142
|
+
assert.equal(eventRequests.length, 2);
|
|
1143
|
+
} finally {
|
|
1144
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1030
1148
|
test('computeApprovalDiff deterministically marks missing allowance/operator checks', () => {
|
|
1031
1149
|
const payload = computeApprovalDiff({
|
|
1032
1150
|
ownerAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|