pandora-cli-skills 1.1.55 → 1.1.57
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/SKILL.md +1 -1
- package/cli/lib/arb_command_service.cjs +71 -6
- package/cli/lib/arbitrage_service.cjs +14 -0
- package/cli/lib/parsers/core_command_flags.cjs +14 -1
- package/cli/lib/polymarket_adapter.cjs +173 -15
- package/cli/lib/scan_command_service.cjs +6 -0
- package/cli/pandora.cjs +443 -34
- package/package.json +1 -1
- package/tests/cli/cli.integration.test.cjs +158 -0
- package/tests/unit/combinatorial_arb.test.cjs +81 -0
- package/tests/unit/core_command_flags.test.cjs +113 -0
- package/tests/unit/polymarket_adapter.test.cjs +81 -0
- package/tests/unit/scan_command_service.test.cjs +68 -0
package/SKILL.md
CHANGED
|
@@ -14,7 +14,7 @@ const ARB_MARKET_FIELDS = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const ARB_USAGE =
|
|
17
|
-
'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--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>]';
|
|
17
|
+
'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--similarity-threshold <0-1>] [--min-token-score <0-1>] [--max-close-diff-hours <n>] [--question-contains <text>] [--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') {
|
|
@@ -87,6 +87,12 @@ function buildMarketSnapshots(markets, orderedIds) {
|
|
|
87
87
|
function buildCrossVenueArbOpportunities(payload, options) {
|
|
88
88
|
const opportunities = Array.isArray(payload && payload.opportunities) ? payload.opportunities : [];
|
|
89
89
|
const minSpreadPct = Number.isFinite(options.minNetSpreadPct) ? Number(options.minNetSpreadPct) : 0;
|
|
90
|
+
const feePctPerLeg = Number.isFinite(options.feePctPerLeg) ? Number(options.feePctPerLeg) : 0;
|
|
91
|
+
const slippagePctPerLeg = Number.isFinite(options.slippagePctPerLeg) ? Number(options.slippagePctPerLeg) : 0;
|
|
92
|
+
const feeImpactPct = roundNumber(Math.max(0, feePctPerLeg) * 2, 6);
|
|
93
|
+
const slippageImpactPct = roundNumber(Math.max(0, slippagePctPerLeg) * 2, 6);
|
|
94
|
+
const totalImpactPct = roundNumber(feeImpactPct + slippageImpactPct, 6);
|
|
95
|
+
const minTvlUsdc = Number.isFinite(options.minTvlUsdc) ? Number(options.minTvlUsdc) : 0;
|
|
90
96
|
const rows = [];
|
|
91
97
|
for (const item of opportunities) {
|
|
92
98
|
const legs = Array.isArray(item.legs) ? item.legs : [];
|
|
@@ -96,7 +102,21 @@ function buildCrossVenueArbOpportunities(payload, options) {
|
|
|
96
102
|
|
|
97
103
|
const spreadYesPct = toFiniteNumber(item.spreadYesPct);
|
|
98
104
|
const spreadNoPct = toFiniteNumber(item.spreadNoPct);
|
|
99
|
-
const
|
|
105
|
+
const grossSpreadPct = Math.max(spreadYesPct === null ? 0 : spreadYesPct, spreadNoPct === null ? 0 : spreadNoPct);
|
|
106
|
+
const pandoraLiquidityUsd = toFiniteNumber(pandoraLeg.liquidityUsd);
|
|
107
|
+
const polymarketLiquidityUsd = toFiniteNumber(polyLeg.liquidityUsd);
|
|
108
|
+
const minLegLiquidityUsd =
|
|
109
|
+
pandoraLiquidityUsd !== null && polymarketLiquidityUsd !== null
|
|
110
|
+
? Math.min(pandoraLiquidityUsd, polymarketLiquidityUsd)
|
|
111
|
+
: null;
|
|
112
|
+
|
|
113
|
+
if (minTvlUsdc > 0) {
|
|
114
|
+
if (minLegLiquidityUsd === null || minLegLiquidityUsd < minTvlUsdc) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const netSpreadPct = grossSpreadPct - totalImpactPct;
|
|
100
120
|
if (netSpreadPct < minSpreadPct) continue;
|
|
101
121
|
|
|
102
122
|
rows.push({
|
|
@@ -109,8 +129,14 @@ function buildCrossVenueArbOpportunities(payload, options) {
|
|
|
109
129
|
polymarketUrl: polyLeg.url || null,
|
|
110
130
|
pandoraYesPct: toFiniteNumber(pandoraLeg.yesPct),
|
|
111
131
|
polymarketYesPct: toFiniteNumber(polyLeg.yesPct),
|
|
132
|
+
pandoraLiquidityUsd,
|
|
133
|
+
polymarketLiquidityUsd,
|
|
134
|
+
minLegLiquidityUsd,
|
|
135
|
+
grossSpreadPct: roundNumber(grossSpreadPct, 6),
|
|
112
136
|
spreadYesPct,
|
|
113
137
|
spreadNoPct,
|
|
138
|
+
feeImpactPct,
|
|
139
|
+
slippageImpactPct,
|
|
114
140
|
netSpreadPct: roundNumber(netSpreadPct, 6),
|
|
115
141
|
confidenceScore: toFiniteNumber(item.confidenceScore),
|
|
116
142
|
riskFlags: Array.isArray(item.riskFlags) ? item.riskFlags : [],
|
|
@@ -314,6 +340,10 @@ function parseArbScanFlags(args, deps) {
|
|
|
314
340
|
amountUsdc: 100,
|
|
315
341
|
combinatorial: false,
|
|
316
342
|
maxBundleSize: 4,
|
|
343
|
+
similarityThreshold: 0.35,
|
|
344
|
+
minTokenScore: 0.12,
|
|
345
|
+
maxCloseDiffHours: 24,
|
|
346
|
+
questionContains: null,
|
|
317
347
|
intervalMs: 5_000,
|
|
318
348
|
iterations: null,
|
|
319
349
|
};
|
|
@@ -380,6 +410,29 @@ function parseArbScanFlags(args, deps) {
|
|
|
380
410
|
i += 1;
|
|
381
411
|
continue;
|
|
382
412
|
}
|
|
413
|
+
if (token === '--similarity-threshold') {
|
|
414
|
+
options.similarityThreshold = parseNumber(requireFlagValue(rest, i, '--similarity-threshold'), '--similarity-threshold');
|
|
415
|
+
i += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (token === '--min-token-score') {
|
|
419
|
+
options.minTokenScore = parseNumber(requireFlagValue(rest, i, '--min-token-score'), '--min-token-score');
|
|
420
|
+
i += 1;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (token === '--max-close-diff-hours') {
|
|
424
|
+
options.maxCloseDiffHours = parsePositiveNumber(
|
|
425
|
+
requireFlagValue(rest, i, '--max-close-diff-hours'),
|
|
426
|
+
'--max-close-diff-hours',
|
|
427
|
+
);
|
|
428
|
+
i += 1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (token === '--question-contains') {
|
|
432
|
+
options.questionContains = String(requireFlagValue(rest, i, '--question-contains')).trim();
|
|
433
|
+
i += 1;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
383
436
|
if (token === '--interval-ms') {
|
|
384
437
|
options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
|
|
385
438
|
i += 1;
|
|
@@ -417,6 +470,12 @@ function parseArbScanFlags(args, deps) {
|
|
|
417
470
|
if (options.slippagePctPerLeg < 0) {
|
|
418
471
|
throw new CliError('INVALID_FLAG_VALUE', '--slippage-pct-per-leg must be >= 0.');
|
|
419
472
|
}
|
|
473
|
+
if (options.similarityThreshold < 0 || options.similarityThreshold > 1) {
|
|
474
|
+
throw new CliError('INVALID_FLAG_VALUE', '--similarity-threshold must be between 0 and 1.');
|
|
475
|
+
}
|
|
476
|
+
if (options.minTokenScore < 0 || options.minTokenScore > 1) {
|
|
477
|
+
throw new CliError('INVALID_FLAG_VALUE', '--min-token-score must be between 0 and 1.');
|
|
478
|
+
}
|
|
420
479
|
|
|
421
480
|
if (!Number.isInteger(options.maxBundleSize) || options.maxBundleSize < 3) {
|
|
422
481
|
throw new CliError('INVALID_FLAG_VALUE', '--max-bundle-size must be an integer >= 3.');
|
|
@@ -530,15 +589,16 @@ function createRunArbCommand(deps) {
|
|
|
530
589
|
timeoutMs: shared.timeoutMs,
|
|
531
590
|
chainId: null,
|
|
532
591
|
venues: ['pandora', 'polymarket'],
|
|
533
|
-
limit: options.limit,
|
|
592
|
+
limit: Math.max(options.limit * 4, 100),
|
|
534
593
|
minSpreadPct: options.minNetSpreadPct,
|
|
535
594
|
minLiquidityUsd: options.minTvlUsdc,
|
|
536
|
-
maxCloseDiffHours:
|
|
537
|
-
similarityThreshold:
|
|
595
|
+
maxCloseDiffHours: options.maxCloseDiffHours,
|
|
596
|
+
similarityThreshold: options.similarityThreshold,
|
|
597
|
+
minTokenScore: options.minTokenScore,
|
|
538
598
|
crossVenueOnly: true,
|
|
539
599
|
withRules: false,
|
|
540
600
|
includeSimilarity: false,
|
|
541
|
-
questionContains:
|
|
601
|
+
questionContains: options.questionContains,
|
|
542
602
|
});
|
|
543
603
|
pairwiseOpportunities = buildCrossVenueArbOpportunities(crossVenuePayload, options);
|
|
544
604
|
combinatorialOpportunities = [];
|
|
@@ -626,6 +686,10 @@ function createRunArbCommand(deps) {
|
|
|
626
686
|
amountUsdc: options.amountUsdc,
|
|
627
687
|
combinatorial: options.combinatorial,
|
|
628
688
|
maxBundleSize: options.maxBundleSize,
|
|
689
|
+
similarityThreshold: options.similarityThreshold,
|
|
690
|
+
minTokenScore: options.minTokenScore,
|
|
691
|
+
maxCloseDiffHours: options.maxCloseDiffHours,
|
|
692
|
+
questionContains: options.questionContains,
|
|
629
693
|
},
|
|
630
694
|
opportunities: iterationSnapshots.flatMap((row) => row.opportunities),
|
|
631
695
|
snapshots: iterationSnapshots,
|
|
@@ -645,6 +709,7 @@ function createRunArbCommand(deps) {
|
|
|
645
709
|
module.exports = {
|
|
646
710
|
ARB_USAGE,
|
|
647
711
|
buildArbOpportunities,
|
|
712
|
+
buildCrossVenueArbOpportunities,
|
|
648
713
|
buildCombinatorialArbOpportunities,
|
|
649
714
|
createRunArbCommand,
|
|
650
715
|
parseArbScanFlags,
|
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
const { toNumber, round } = require('./shared/utils.cjs');
|
|
9
9
|
|
|
10
10
|
const ARBITRAGE_SCHEMA_VERSION = '1.1.0';
|
|
11
|
+
const DEFAULT_MIN_TOKEN_SCORE = 0.12;
|
|
11
12
|
|
|
12
13
|
function toUsdc(raw) {
|
|
13
14
|
const numeric = toNumber(raw);
|
|
@@ -170,6 +171,9 @@ async function fetchPandoraLegs(options, diagnostics) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
function buildGroups(legs, options) {
|
|
174
|
+
const minTokenScore = Number.isFinite(options && options.minTokenScore)
|
|
175
|
+
? Number(options.minTokenScore)
|
|
176
|
+
: DEFAULT_MIN_TOKEN_SCORE;
|
|
173
177
|
const parent = new Map();
|
|
174
178
|
const acceptedPairChecks = new Map();
|
|
175
179
|
const makePairKey = (a, b) => [a, b].sort().join('|');
|
|
@@ -202,6 +206,7 @@ function buildGroups(legs, options) {
|
|
|
202
206
|
if (options.crossVenueOnly && left.venue === right.venue) continue;
|
|
203
207
|
|
|
204
208
|
const similarity = questionSimilarityBreakdown(left.question, right.question);
|
|
209
|
+
if (similarity.tokenScore < minTokenScore) continue;
|
|
205
210
|
if (similarity.score < options.similarityThreshold) continue;
|
|
206
211
|
|
|
207
212
|
let closeDiffHours = null;
|
|
@@ -587,6 +592,9 @@ async function scanArbitrage(options) {
|
|
|
587
592
|
timeoutMs: options.timeoutMs,
|
|
588
593
|
limit: Math.max(options.limit * 3, 100),
|
|
589
594
|
});
|
|
595
|
+
if (Array.isArray(poly.diagnostics) && poly.diagnostics.length) {
|
|
596
|
+
diagnostics.push(...poly.diagnostics);
|
|
597
|
+
}
|
|
590
598
|
|
|
591
599
|
const filtered = poly.items.filter((item) => {
|
|
592
600
|
if (!item.question) return false;
|
|
@@ -627,6 +635,9 @@ async function scanArbitrage(options) {
|
|
|
627
635
|
minLiquidityUsd: options.minLiquidityUsd,
|
|
628
636
|
maxCloseDiffHours: options.maxCloseDiffHours,
|
|
629
637
|
similarityThreshold: options.similarityThreshold,
|
|
638
|
+
minTokenScore: Number.isFinite(options.minTokenScore)
|
|
639
|
+
? options.minTokenScore
|
|
640
|
+
: DEFAULT_MIN_TOKEN_SCORE,
|
|
630
641
|
crossVenueOnly: options.crossVenueOnly,
|
|
631
642
|
withRules: options.withRules,
|
|
632
643
|
includeSimilarity: options.includeSimilarity,
|
|
@@ -694,6 +705,9 @@ async function scanArbitrage(options) {
|
|
|
694
705
|
minLiquidityUsd: options.minLiquidityUsd,
|
|
695
706
|
maxCloseDiffHours: options.maxCloseDiffHours,
|
|
696
707
|
similarityThreshold: options.similarityThreshold,
|
|
708
|
+
minTokenScore: Number.isFinite(options.minTokenScore)
|
|
709
|
+
? options.minTokenScore
|
|
710
|
+
: DEFAULT_MIN_TOKEN_SCORE,
|
|
697
711
|
crossVenueOnly: options.crossVenueOnly,
|
|
698
712
|
withRules: options.withRules,
|
|
699
713
|
includeSimilarity: options.includeSimilarity,
|
|
@@ -1153,6 +1153,7 @@ function createCoreCommandFlagParsers(deps) {
|
|
|
1153
1153
|
minLiquidityUsd: 1000,
|
|
1154
1154
|
maxCloseDiffHours: 24,
|
|
1155
1155
|
similarityThreshold: 0.86,
|
|
1156
|
+
minTokenScore: 0.12,
|
|
1156
1157
|
crossVenueOnly: true,
|
|
1157
1158
|
withRules: false,
|
|
1158
1159
|
includeSimilarity: false,
|
|
@@ -1186,7 +1187,7 @@ function createCoreCommandFlagParsers(deps) {
|
|
|
1186
1187
|
continue;
|
|
1187
1188
|
}
|
|
1188
1189
|
if (token === '--min-spread-pct') {
|
|
1189
|
-
options.minSpreadPct =
|
|
1190
|
+
options.minSpreadPct = parseNumber(requireFlagValue(args, i, '--min-spread-pct'), '--min-spread-pct');
|
|
1190
1191
|
i += 1;
|
|
1191
1192
|
continue;
|
|
1192
1193
|
}
|
|
@@ -1214,6 +1215,14 @@ function createCoreCommandFlagParsers(deps) {
|
|
|
1214
1215
|
i += 1;
|
|
1215
1216
|
continue;
|
|
1216
1217
|
}
|
|
1218
|
+
if (token === '--min-token-score') {
|
|
1219
|
+
options.minTokenScore = parseNumber(requireFlagValue(args, i, '--min-token-score'), '--min-token-score');
|
|
1220
|
+
if (options.minTokenScore < 0 || options.minTokenScore > 1) {
|
|
1221
|
+
throw new CliError('INVALID_FLAG_VALUE', '--min-token-score must be between 0 and 1.');
|
|
1222
|
+
}
|
|
1223
|
+
i += 1;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1217
1226
|
if (token === '--cross-venue-only') {
|
|
1218
1227
|
options.crossVenueOnly = true;
|
|
1219
1228
|
continue;
|
|
@@ -1249,6 +1258,10 @@ function createCoreCommandFlagParsers(deps) {
|
|
|
1249
1258
|
throw new CliError('UNKNOWN_FLAG', `Unknown flag for arbitrage: ${token}`);
|
|
1250
1259
|
}
|
|
1251
1260
|
|
|
1261
|
+
if (options.minSpreadPct < 0) {
|
|
1262
|
+
throw new CliError('INVALID_FLAG_VALUE', '--min-spread-pct must be >= 0.');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1252
1265
|
return options;
|
|
1253
1266
|
}
|
|
1254
1267
|
|
|
@@ -2,6 +2,7 @@ const { ClobClient, Chain } = require('@polymarket/clob-client');
|
|
|
2
2
|
const { toNumber } = require('./shared/utils.cjs');
|
|
3
3
|
|
|
4
4
|
const DEFAULT_POLYMARKET_HOST = 'https://clob.polymarket.com';
|
|
5
|
+
const DEFAULT_POLYMARKET_GAMMA_HOST = 'https://gamma-api.polymarket.com';
|
|
5
6
|
|
|
6
7
|
function toTimestampSeconds(value) {
|
|
7
8
|
if (!value) return null;
|
|
@@ -53,6 +54,66 @@ function normalizeTokens(tokens) {
|
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function safeParseJsonArray(value) {
|
|
58
|
+
if (Array.isArray(value)) return value;
|
|
59
|
+
if (typeof value !== 'string') return null;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(value);
|
|
62
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildTokensFromGammaPayload(row) {
|
|
69
|
+
const outcomes = safeParseJsonArray(row && row.outcomes) || safeParseJsonArray(row && row.outcomeNames);
|
|
70
|
+
const prices = safeParseJsonArray(row && row.outcomePrices) || safeParseJsonArray(row && row.prices);
|
|
71
|
+
if (!Array.isArray(outcomes) || !Array.isArray(prices) || outcomes.length !== prices.length || !outcomes.length) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const tokens = [];
|
|
75
|
+
for (let index = 0; index < outcomes.length; index += 1) {
|
|
76
|
+
tokens.push({
|
|
77
|
+
outcome: String(outcomes[index] || ''),
|
|
78
|
+
price: prices[index],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return tokens;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mapGammaRow(row) {
|
|
85
|
+
const tokens =
|
|
86
|
+
(Array.isArray(row && row.tokens) ? row.tokens : null) ||
|
|
87
|
+
(Array.isArray(row && row.outcomePrices) && Array.isArray(row && row.outcomes)
|
|
88
|
+
? row.outcomes.map((outcome, index) => ({ outcome, price: row.outcomePrices[index] }))
|
|
89
|
+
: null) ||
|
|
90
|
+
buildTokensFromGammaPayload(row);
|
|
91
|
+
const mapped = normalizeTokens(tokens || []);
|
|
92
|
+
const marketId = row && (row.conditionId || row.condition_id || row.id || row.questionID) ? String(
|
|
93
|
+
row.conditionId || row.condition_id || row.id || row.questionID,
|
|
94
|
+
) : null;
|
|
95
|
+
const closeTimestamp = toTimestampSeconds(
|
|
96
|
+
row && (row.endDateIso || row.end_date_iso || row.endDate || row.game_start_time || row.closedTime),
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
legId: `polymarket:${String(marketId || '')}`,
|
|
100
|
+
venue: 'polymarket',
|
|
101
|
+
marketId,
|
|
102
|
+
question: row && (row.question || row.title || row.description) ? String(row.question || row.title || row.description) : null,
|
|
103
|
+
closeTimestamp,
|
|
104
|
+
yesPct: mapped.yes,
|
|
105
|
+
noPct: mapped.no,
|
|
106
|
+
liquidityUsd: toNumber(row && (row.liquidityNum || row.liquidity || row.liquidityClob)),
|
|
107
|
+
volumeUsd: toNumber(row && (row.volumeNum || row.volume || row.volumeClob)),
|
|
108
|
+
url: row && (row.market_slug || row.slug) ? `https://polymarket.com/event/${String(row.market_slug || row.slug)}` : null,
|
|
109
|
+
oddsSource: 'polymarket:gamma-markets',
|
|
110
|
+
diagnostics: mapped.diagnostics,
|
|
111
|
+
rules: row && row.description ? String(row.description) : null,
|
|
112
|
+
sources: [],
|
|
113
|
+
pollStatus: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
56
117
|
function mapPolymarketRow(row) {
|
|
57
118
|
const mapped = normalizeTokens(row.tokens || []);
|
|
58
119
|
const question = row.question || row.description || null;
|
|
@@ -101,41 +162,138 @@ async function fetchMockPolymarketMarkets(mockUrl, timeoutMs) {
|
|
|
101
162
|
}
|
|
102
163
|
}
|
|
103
164
|
|
|
104
|
-
async function
|
|
105
|
-
const
|
|
165
|
+
async function fetchJsonWithTimeout(url, timeoutMs) {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
method: 'GET',
|
|
171
|
+
headers: { accept: 'application/json' },
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
176
|
+
}
|
|
177
|
+
return response.json();
|
|
178
|
+
} finally {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeGammaMarketRows(payload) {
|
|
184
|
+
if (Array.isArray(payload)) return payload;
|
|
185
|
+
if (payload && Array.isArray(payload.data)) return payload.data;
|
|
186
|
+
if (payload && Array.isArray(payload.markets)) return payload.markets;
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fetchGammaPolymarketMarkets(options = {}) {
|
|
191
|
+
const gammaHostRaw =
|
|
192
|
+
options.gammaHost ||
|
|
193
|
+
process.env.POLYMARKET_GAMMA_HOST ||
|
|
194
|
+
DEFAULT_POLYMARKET_GAMMA_HOST;
|
|
195
|
+
const gammaHost = String(gammaHostRaw || '').replace(/\/+$/, '');
|
|
106
196
|
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
|
|
107
197
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
108
198
|
|
|
199
|
+
const rows = [];
|
|
200
|
+
let offset = 0;
|
|
201
|
+
let loopCount = 0;
|
|
202
|
+
while (rows.length < limit && loopCount < 10) {
|
|
203
|
+
loopCount += 1;
|
|
204
|
+
const pageLimit = Math.min(200, Math.max(25, limit - rows.length));
|
|
205
|
+
const url =
|
|
206
|
+
`${gammaHost}/markets?active=true&closed=false&archived=false&order=volume` +
|
|
207
|
+
`&ascending=false&limit=${pageLimit}&offset=${offset}`;
|
|
208
|
+
const payload = await fetchJsonWithTimeout(url, timeoutMs);
|
|
209
|
+
const batch = normalizeGammaMarketRows(payload);
|
|
210
|
+
if (!batch.length) break;
|
|
211
|
+
rows.push(...batch);
|
|
212
|
+
if (batch.length < pageLimit) break;
|
|
213
|
+
offset += batch.length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
host: gammaHost,
|
|
218
|
+
rows,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function fetchClobPolymarketMarkets(options = {}) {
|
|
223
|
+
const host = options.host || DEFAULT_POLYMARKET_HOST;
|
|
224
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
|
|
225
|
+
const client = new ClobClient(host, Chain.POLYGON);
|
|
226
|
+
const rows = [];
|
|
227
|
+
let cursor;
|
|
228
|
+
let loops = 0;
|
|
229
|
+
while (rows.length < limit && loops < 8) {
|
|
230
|
+
loops += 1;
|
|
231
|
+
const page = cursor ? await client.getMarkets(cursor) : await client.getMarkets();
|
|
232
|
+
const data = Array.isArray(page && page.data) ? page.data : [];
|
|
233
|
+
rows.push(...data);
|
|
234
|
+
if (!page || !page.next_cursor || page.next_cursor === cursor) break;
|
|
235
|
+
cursor = page.next_cursor;
|
|
236
|
+
}
|
|
237
|
+
return { host, rows };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function fetchPolymarketMarkets(options = {}) {
|
|
241
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
|
|
242
|
+
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
243
|
+
const diagnostics = [];
|
|
109
244
|
let rows = [];
|
|
245
|
+
let source = 'polymarket:clob';
|
|
246
|
+
let host = options.host || DEFAULT_POLYMARKET_HOST;
|
|
110
247
|
|
|
111
248
|
if (options.mockUrl) {
|
|
112
249
|
rows = await fetchMockPolymarketMarkets(options.mockUrl, timeoutMs);
|
|
250
|
+
source = 'polymarket:mock';
|
|
113
251
|
} else {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
252
|
+
let gammaResult = null;
|
|
253
|
+
try {
|
|
254
|
+
const preferredGammaHost =
|
|
255
|
+
options.host && /gamma-api\.polymarket\.com/i.test(String(options.host))
|
|
256
|
+
? options.host
|
|
257
|
+
: options.gammaHost;
|
|
258
|
+
gammaResult = await fetchGammaPolymarketMarkets({
|
|
259
|
+
gammaHost: preferredGammaHost,
|
|
260
|
+
timeoutMs,
|
|
261
|
+
limit,
|
|
262
|
+
});
|
|
263
|
+
if (Array.isArray(gammaResult.rows) && gammaResult.rows.length) {
|
|
264
|
+
rows = gammaResult.rows;
|
|
265
|
+
source = 'polymarket:gamma';
|
|
266
|
+
host = gammaResult.host;
|
|
124
267
|
}
|
|
125
|
-
|
|
268
|
+
} catch (err) {
|
|
269
|
+
diagnostics.push(`Gamma markets fetch failed: ${err && err.message ? err.message : String(err)}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!rows.length) {
|
|
273
|
+
const clob = await fetchClobPolymarketMarkets({
|
|
274
|
+
host: options.host,
|
|
275
|
+
timeoutMs,
|
|
276
|
+
limit,
|
|
277
|
+
});
|
|
278
|
+
rows = clob.rows;
|
|
279
|
+
source = 'polymarket:clob';
|
|
280
|
+
host = clob.host;
|
|
126
281
|
}
|
|
127
282
|
}
|
|
128
283
|
|
|
129
|
-
const
|
|
284
|
+
const mapper = source === 'polymarket:gamma' ? mapGammaRow : mapPolymarketRow;
|
|
285
|
+
const mapped = rows.slice(0, limit).map(mapper);
|
|
130
286
|
return {
|
|
131
287
|
host,
|
|
132
|
-
source
|
|
288
|
+
source,
|
|
133
289
|
count: mapped.length,
|
|
134
290
|
items: mapped,
|
|
291
|
+
diagnostics,
|
|
135
292
|
};
|
|
136
293
|
}
|
|
137
294
|
|
|
138
295
|
module.exports = {
|
|
139
296
|
DEFAULT_POLYMARKET_HOST,
|
|
297
|
+
DEFAULT_POLYMARKET_GAMMA_HOST,
|
|
140
298
|
fetchPolymarketMarkets,
|
|
141
299
|
};
|
|
@@ -58,8 +58,10 @@ function createRunScanCommand(deps) {
|
|
|
58
58
|
const indexerUrl = resolveIndexerUrl(shared.indexerUrl);
|
|
59
59
|
|
|
60
60
|
const options = parseMarketsListFlags(shared.rest);
|
|
61
|
+
options.expand = true;
|
|
61
62
|
options.withOdds = true;
|
|
62
63
|
|
|
64
|
+
let hedgeableDiagnostics = [];
|
|
63
65
|
let { items, pageInfo, unfilteredCount } = await fetchMarketsListPage(indexerUrl, options, shared.timeoutMs);
|
|
64
66
|
if (options.hedgeable && typeof filterHedgeableMarkets === 'function') {
|
|
65
67
|
const filtered = await filterHedgeableMarkets({
|
|
@@ -72,6 +74,9 @@ function createRunScanCommand(deps) {
|
|
|
72
74
|
if (typeof filtered.unfilteredCount === 'number') {
|
|
73
75
|
unfilteredCount = filtered.unfilteredCount;
|
|
74
76
|
}
|
|
77
|
+
if (Array.isArray(filtered && filtered.diagnostics)) {
|
|
78
|
+
hedgeableDiagnostics = filtered.diagnostics;
|
|
79
|
+
}
|
|
75
80
|
}
|
|
76
81
|
const enrichmentContext = await buildMarketsEnrichmentContext(indexerUrl, items, options, shared.timeoutMs);
|
|
77
82
|
const payload = buildMarketsListPayload(indexerUrl, options, items, pageInfo, {
|
|
@@ -79,6 +84,7 @@ function createRunScanCommand(deps) {
|
|
|
79
84
|
scanMode: true,
|
|
80
85
|
enrichmentContext,
|
|
81
86
|
unfilteredCount,
|
|
87
|
+
externalDiagnostics: hedgeableDiagnostics,
|
|
82
88
|
});
|
|
83
89
|
|
|
84
90
|
emitSuccess(context.outputMode, 'scan', payload, renderScanTable);
|