pandora-cli-skills 1.1.50 → 1.1.52
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 +10 -0
- package/SKILL.md +1 -1
- package/cli/lib/mirror_command_service.cjs +1 -1
- package/cli/lib/mirror_handlers/browse.cjs +2 -2
- package/cli/lib/mirror_service.cjs +5 -0
- package/cli/lib/mirror_sync_service.cjs +126 -65
- package/cli/lib/parsers/mirror_remaining_flags.cjs +117 -4
- package/cli/lib/polymarket_trade_adapter.cjs +261 -5
- package/cli/lib/schema_command_service.cjs +1 -1
- package/cli/pandora.cjs +6 -2
- package/package.json +3 -1
- package/tests/cli/cli.integration.test.cjs +120 -0
- package/tests/unit/new-features.test.cjs +369 -0
package/README.md
CHANGED
|
@@ -73,6 +73,16 @@ pandora --output json lifecycle status --id <lifecycle-id>
|
|
|
73
73
|
pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Mega Analytics Page (Dune Panels)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# starts local page at http://127.0.0.1:8787
|
|
80
|
+
npm run analytics:mega
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- Panel registry source: `analytics/dune/panel_registry.json`
|
|
84
|
+
- Dashboard creation script reuses this registry: `scripts/create_pandora_dashboard.js`
|
|
85
|
+
|
|
76
86
|
## Risk Controls
|
|
77
87
|
|
|
78
88
|
- Inspect/engage panic lock:
|
package/SKILL.md
CHANGED
|
@@ -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-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>]',
|
|
94
|
+
' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>|--end-date-after <date|72h>] [--closes-before <date>|--end-date-before <date|72h>] [--question-contains <text>|--keyword <text>] [--slug <text>] [--category sports|crypto|politics|entertainment] [--exclude-sports] [--sort-by volume24h|liquidity|endDate] [--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>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
25
|
+
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>|--end-date-after <date|72h>] [--closes-before <date>|--end-date-before <date|72h>] [--question-contains <text>|--keyword <text>] [--slug <text>] [--category sports|crypto|politics|entertainment] [--exclude-sports] [--sort-by volume24h|liquidity|endDate] [--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>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
30
|
+
'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>|--end-date-after <date|72h>] [--closes-before <date>|--end-date-before <date|72h>] [--question-contains <text>|--keyword <text>] [--slug <text>] [--category sports|crypto|politics|entertainment] [--exclude-sports] [--sort-by volume24h|liquidity|endDate] [--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;
|
|
@@ -413,6 +413,11 @@ async function browseMirrorMarkets(options = {}) {
|
|
|
413
413
|
closesAfter: options.closesAfter,
|
|
414
414
|
closesBefore: options.closesBefore,
|
|
415
415
|
questionContains: options.questionContains,
|
|
416
|
+
keyword: options.keyword,
|
|
417
|
+
slug: options.slug,
|
|
418
|
+
categories: Array.isArray(options.categories) ? options.categories : [],
|
|
419
|
+
excludeSports: Boolean(options.excludeSports),
|
|
420
|
+
sortBy: options.sortBy,
|
|
416
421
|
limit: options.limit,
|
|
417
422
|
});
|
|
418
423
|
|
|
@@ -102,82 +102,143 @@ async function runMirrorSync(options, deps = {}) {
|
|
|
102
102
|
break;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
try {
|
|
106
|
+
resetDailyCountersIfNeeded(state, tickAt);
|
|
107
|
+
|
|
108
|
+
const verifyPayload =
|
|
109
|
+
iteration === 1 && startupVerifyPayload
|
|
110
|
+
? startupVerifyPayload
|
|
111
|
+
: await verifyFn(buildVerifyRequest(options));
|
|
112
|
+
|
|
113
|
+
const snapshotMetrics = evaluateSnapshot(verifyPayload, options);
|
|
114
|
+
const plan = buildTickPlan({
|
|
115
|
+
snapshotMetrics,
|
|
116
|
+
state,
|
|
117
|
+
options,
|
|
118
|
+
});
|
|
119
|
+
const depth = await fetchDepthSnapshot({
|
|
120
|
+
depthFn,
|
|
121
|
+
verifyPayload,
|
|
122
|
+
options,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const gate = applyGateBypassPolicy(
|
|
126
|
+
evaluateStrictGates(
|
|
127
|
+
buildTickGateContext({
|
|
128
|
+
verifyPayload,
|
|
129
|
+
options,
|
|
130
|
+
state,
|
|
131
|
+
plan,
|
|
132
|
+
snapshotMetrics,
|
|
133
|
+
depth,
|
|
134
|
+
minimumTimeToCloseSec,
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
options,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const snapshot = buildTickSnapshot({
|
|
141
|
+
iteration,
|
|
142
|
+
tickAt,
|
|
143
|
+
verifyPayload,
|
|
144
|
+
options,
|
|
145
|
+
snapshotMetrics,
|
|
146
|
+
plan,
|
|
147
|
+
depth,
|
|
148
|
+
gate,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
|
|
152
|
+
await processTriggeredAction({
|
|
128
153
|
options,
|
|
129
154
|
state,
|
|
155
|
+
snapshot,
|
|
130
156
|
plan,
|
|
157
|
+
gate,
|
|
158
|
+
tickAt,
|
|
159
|
+
loadedFilePath: loaded.filePath,
|
|
160
|
+
rebalanceFn,
|
|
161
|
+
hedgeFn,
|
|
162
|
+
sendWebhook,
|
|
163
|
+
strategyHash: hash,
|
|
164
|
+
iteration,
|
|
165
|
+
actions,
|
|
166
|
+
webhookReports,
|
|
131
167
|
snapshotMetrics,
|
|
168
|
+
verifyPayload,
|
|
132
169
|
depth,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const snapshot = buildTickSnapshot({
|
|
140
|
-
iteration,
|
|
141
|
-
tickAt,
|
|
142
|
-
verifyPayload,
|
|
143
|
-
options,
|
|
144
|
-
snapshotMetrics,
|
|
145
|
-
plan,
|
|
146
|
-
depth,
|
|
147
|
-
gate,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
|
|
151
|
-
await processTriggeredAction({
|
|
152
|
-
options,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await persistTickSnapshot({
|
|
174
|
+
loadedFilePath: loaded.filePath,
|
|
153
175
|
state,
|
|
154
|
-
snapshot,
|
|
155
|
-
plan,
|
|
156
|
-
gate,
|
|
157
176
|
tickAt,
|
|
177
|
+
snapshot,
|
|
178
|
+
snapshots,
|
|
179
|
+
onTick,
|
|
180
|
+
iteration,
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const errorCode = err && err.code ? String(err.code) : 'MIRROR_SYNC_TICK_FAILED';
|
|
184
|
+
const errorMessage = err && err.message ? err.message : String(err);
|
|
185
|
+
const errorDetails = err && err.details !== undefined ? err.details : null;
|
|
186
|
+
const timestamp = tickAt.toISOString();
|
|
187
|
+
|
|
188
|
+
const diagnostic = {
|
|
189
|
+
level: 'error',
|
|
190
|
+
scope: 'tick',
|
|
191
|
+
iteration,
|
|
192
|
+
timestamp,
|
|
193
|
+
code: errorCode,
|
|
194
|
+
message: errorMessage,
|
|
195
|
+
retryable: options.mode !== 'once',
|
|
196
|
+
};
|
|
197
|
+
if (errorDetails !== null) diagnostic.details = errorDetails;
|
|
198
|
+
diagnostics.push(diagnostic);
|
|
199
|
+
|
|
200
|
+
const snapshot = {
|
|
201
|
+
schemaVersion: MIRROR_SYNC_SCHEMA_VERSION,
|
|
202
|
+
timestamp,
|
|
203
|
+
iteration,
|
|
204
|
+
metrics: {
|
|
205
|
+
driftBps: null,
|
|
206
|
+
plannedRebalanceUsdc: 0,
|
|
207
|
+
plannedHedgeUsdc: 0,
|
|
208
|
+
},
|
|
209
|
+
strictGate: {
|
|
210
|
+
ok: false,
|
|
211
|
+
failedChecks: [],
|
|
212
|
+
checks: [],
|
|
213
|
+
},
|
|
214
|
+
action: {
|
|
215
|
+
status: 'error',
|
|
216
|
+
failedChecks: [],
|
|
217
|
+
forcedGateBypass: false,
|
|
218
|
+
errorCode,
|
|
219
|
+
errorMessage,
|
|
220
|
+
},
|
|
221
|
+
error: {
|
|
222
|
+
code: errorCode,
|
|
223
|
+
message: errorMessage,
|
|
224
|
+
details: errorDetails,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await persistTickSnapshot({
|
|
158
229
|
loadedFilePath: loaded.filePath,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
230
|
+
state,
|
|
231
|
+
tickAt,
|
|
232
|
+
snapshot,
|
|
233
|
+
snapshots,
|
|
234
|
+
onTick,
|
|
163
235
|
iteration,
|
|
164
|
-
actions,
|
|
165
|
-
webhookReports,
|
|
166
|
-
snapshotMetrics,
|
|
167
|
-
verifyPayload,
|
|
168
|
-
depth,
|
|
169
236
|
});
|
|
170
|
-
}
|
|
171
237
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
snapshot,
|
|
177
|
-
snapshots,
|
|
178
|
-
onTick,
|
|
179
|
-
iteration,
|
|
180
|
-
});
|
|
238
|
+
if (options.mode === 'once') {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
181
242
|
|
|
182
243
|
if (shouldStop) break;
|
|
183
244
|
if (iteration >= maxIterations) break;
|
|
@@ -25,6 +25,71 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
25
25
|
const parseDateLikeFlag = requireDep(deps, 'parseDateLikeFlag');
|
|
26
26
|
const parsePositiveInteger = requireDep(deps, 'parsePositiveInteger');
|
|
27
27
|
const parseInteger = requireDep(deps, 'parseInteger');
|
|
28
|
+
const allowedSortBy = new Set(['volume24h', 'liquidity', 'endDate']);
|
|
29
|
+
const allowedCategories = new Set(['sports', 'crypto', 'politics', 'entertainment']);
|
|
30
|
+
|
|
31
|
+
function parseBrowseWindowValue(value, flagName) {
|
|
32
|
+
const text = String(value || '').trim();
|
|
33
|
+
const relativeMatch = /^([1-9]\d*)([smhdw])$/i.exec(text);
|
|
34
|
+
if (relativeMatch) {
|
|
35
|
+
const quantity = Number(relativeMatch[1]);
|
|
36
|
+
const unit = String(relativeMatch[2] || '').toLowerCase();
|
|
37
|
+
const unitMs = {
|
|
38
|
+
s: 1000,
|
|
39
|
+
m: 60 * 1000,
|
|
40
|
+
h: 60 * 60 * 1000,
|
|
41
|
+
d: 24 * 60 * 60 * 1000,
|
|
42
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
43
|
+
};
|
|
44
|
+
const multiplier = unitMs[unit];
|
|
45
|
+
if (!multiplier) {
|
|
46
|
+
throw new CliError(
|
|
47
|
+
'INVALID_FLAG_VALUE',
|
|
48
|
+
`${flagName} relative window must use one of s|m|h|d|w (example: 72h). Received: "${text}"`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return new Date(Date.now() + quantity * multiplier).toISOString();
|
|
52
|
+
}
|
|
53
|
+
return parseDateLikeFlag(text, flagName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseSortBy(rawValue) {
|
|
57
|
+
const text = String(rawValue || '').trim().toLowerCase();
|
|
58
|
+
if (!text) {
|
|
59
|
+
throw new CliError('INVALID_FLAG_VALUE', '--sort-by requires a value: volume24h|liquidity|endDate.');
|
|
60
|
+
}
|
|
61
|
+
if (text === 'volume' || text === 'volume24h' || text === 'volume24husd') return 'volume24h';
|
|
62
|
+
if (text === 'liquidity' || text === 'liquidityusd') return 'liquidity';
|
|
63
|
+
if (text === 'enddate' || text === 'end-date' || text === 'close' || text === 'close-time' || text === 'closetimestamp') {
|
|
64
|
+
return 'endDate';
|
|
65
|
+
}
|
|
66
|
+
throw new CliError(
|
|
67
|
+
'INVALID_FLAG_VALUE',
|
|
68
|
+
`--sort-by must be one of volume24h|liquidity|endDate. Received: "${rawValue}"`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseCategoryList(rawValue, flagName) {
|
|
73
|
+
const values = String(rawValue || '')
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((value) => value.trim().toLowerCase())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
if (!values.length) {
|
|
78
|
+
throw new CliError(
|
|
79
|
+
'INVALID_FLAG_VALUE',
|
|
80
|
+
`${flagName} must include at least one category: sports|crypto|politics|entertainment.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
if (!allowedCategories.has(value)) {
|
|
85
|
+
throw new CliError(
|
|
86
|
+
'INVALID_FLAG_VALUE',
|
|
87
|
+
`${flagName} contains unsupported category "${value}". Allowed: sports|crypto|politics|entertainment.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return Array.from(new Set(values));
|
|
92
|
+
}
|
|
28
93
|
|
|
29
94
|
return function parseMirrorBrowseFlags(args) {
|
|
30
95
|
const options = {
|
|
@@ -40,6 +105,11 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
40
105
|
polymarketGammaMockUrl: null,
|
|
41
106
|
polymarketMockUrl: null,
|
|
42
107
|
polymarketTagIds: [],
|
|
108
|
+
categories: [],
|
|
109
|
+
excludeSports: false,
|
|
110
|
+
sortBy: 'volume24h',
|
|
111
|
+
keyword: null,
|
|
112
|
+
slug: null,
|
|
43
113
|
};
|
|
44
114
|
|
|
45
115
|
function pushTagId(rawValue, flagName) {
|
|
@@ -63,13 +133,13 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
63
133
|
i += 1;
|
|
64
134
|
continue;
|
|
65
135
|
}
|
|
66
|
-
if (token === '--closes-after') {
|
|
67
|
-
options.closesAfter =
|
|
136
|
+
if (token === '--closes-after' || token === '--end-date-after') {
|
|
137
|
+
options.closesAfter = parseBrowseWindowValue(requireFlagValue(args, i, token), token);
|
|
68
138
|
i += 1;
|
|
69
139
|
continue;
|
|
70
140
|
}
|
|
71
|
-
if (token === '--closes-before') {
|
|
72
|
-
options.closesBefore =
|
|
141
|
+
if (token === '--closes-before' || token === '--end-date-before') {
|
|
142
|
+
options.closesBefore = parseBrowseWindowValue(requireFlagValue(args, i, token), token);
|
|
73
143
|
i += 1;
|
|
74
144
|
continue;
|
|
75
145
|
}
|
|
@@ -78,11 +148,36 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
78
148
|
i += 1;
|
|
79
149
|
continue;
|
|
80
150
|
}
|
|
151
|
+
if (token === '--keyword') {
|
|
152
|
+
options.keyword = requireFlagValue(args, i, '--keyword');
|
|
153
|
+
i += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (token === '--slug') {
|
|
157
|
+
options.slug = requireFlagValue(args, i, '--slug');
|
|
158
|
+
i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
81
161
|
if (token === '--limit') {
|
|
82
162
|
options.limit = parsePositiveInteger(requireFlagValue(args, i, '--limit'), '--limit');
|
|
83
163
|
i += 1;
|
|
84
164
|
continue;
|
|
85
165
|
}
|
|
166
|
+
if (token === '--sort-by') {
|
|
167
|
+
options.sortBy = parseSortBy(requireFlagValue(args, i, '--sort-by'));
|
|
168
|
+
i += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (token === '--category') {
|
|
172
|
+
const parsed = parseCategoryList(requireFlagValue(args, i, '--category'), '--category');
|
|
173
|
+
options.categories = Array.from(new Set(options.categories.concat(parsed)));
|
|
174
|
+
i += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (token === '--exclude-sports') {
|
|
178
|
+
options.excludeSports = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
86
181
|
if (token === '--chain-id') {
|
|
87
182
|
options.chainId = parseInteger(requireFlagValue(args, i, '--chain-id'), '--chain-id');
|
|
88
183
|
i += 1;
|
|
@@ -129,10 +224,28 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
129
224
|
if (options.minYesPct !== null && options.maxYesPct !== null && options.minYesPct > options.maxYesPct) {
|
|
130
225
|
throw new CliError('INVALID_ARGS', '--min-yes-pct cannot be greater than --max-yes-pct.');
|
|
131
226
|
}
|
|
227
|
+
if (
|
|
228
|
+
options.closesAfter &&
|
|
229
|
+
options.closesBefore &&
|
|
230
|
+
Number.isFinite(Date.parse(options.closesAfter)) &&
|
|
231
|
+
Number.isFinite(Date.parse(options.closesBefore)) &&
|
|
232
|
+
Date.parse(options.closesAfter) > Date.parse(options.closesBefore)
|
|
233
|
+
) {
|
|
234
|
+
throw new CliError('INVALID_ARGS', '--closes-after/--end-date-after cannot be later than --closes-before/--end-date-before.');
|
|
235
|
+
}
|
|
236
|
+
if (options.excludeSports && options.categories.includes('sports')) {
|
|
237
|
+
throw new CliError('INVALID_ARGS', '--exclude-sports cannot be combined with --category sports.');
|
|
238
|
+
}
|
|
132
239
|
|
|
133
240
|
if (options.polymarketTagIds.length) {
|
|
134
241
|
options.polymarketTagIds = Array.from(new Set(options.polymarketTagIds));
|
|
135
242
|
}
|
|
243
|
+
if (options.categories.length) {
|
|
244
|
+
options.categories = Array.from(new Set(options.categories));
|
|
245
|
+
}
|
|
246
|
+
if (allowedSortBy.has(options.sortBy) !== true) {
|
|
247
|
+
throw new CliError('INVALID_FLAG_VALUE', `Unsupported --sort-by value: "${options.sortBy}"`);
|
|
248
|
+
}
|
|
136
249
|
|
|
137
250
|
return options;
|
|
138
251
|
};
|
|
@@ -616,6 +616,184 @@ function normalizeTagIdList(input) {
|
|
|
616
616
|
return Array.from(new Set(normalized));
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
+
const BROWSE_ALLOWED_CATEGORIES = new Set(['sports', 'crypto', 'politics', 'entertainment']);
|
|
620
|
+
const BROWSE_DEFAULT_SPORT_TAG_IDS = Object.freeze([82, 100350]);
|
|
621
|
+
const BROWSE_SPORT_KEYWORDS = [
|
|
622
|
+
'sport',
|
|
623
|
+
'soccer',
|
|
624
|
+
'football',
|
|
625
|
+
'premier league',
|
|
626
|
+
'epl',
|
|
627
|
+
'nba',
|
|
628
|
+
'nfl',
|
|
629
|
+
'nhl',
|
|
630
|
+
'mlb',
|
|
631
|
+
'tennis',
|
|
632
|
+
'cricket',
|
|
633
|
+
'mma',
|
|
634
|
+
'ufc',
|
|
635
|
+
'formula 1',
|
|
636
|
+
'f1',
|
|
637
|
+
];
|
|
638
|
+
const BROWSE_CRYPTO_KEYWORDS = [
|
|
639
|
+
'crypto',
|
|
640
|
+
'bitcoin',
|
|
641
|
+
'btc',
|
|
642
|
+
'ethereum',
|
|
643
|
+
'eth',
|
|
644
|
+
'solana',
|
|
645
|
+
'defi',
|
|
646
|
+
'blockchain',
|
|
647
|
+
'altcoin',
|
|
648
|
+
'memecoin',
|
|
649
|
+
];
|
|
650
|
+
const BROWSE_POLITICS_KEYWORDS = [
|
|
651
|
+
'politic',
|
|
652
|
+
'election',
|
|
653
|
+
'president',
|
|
654
|
+
'prime minister',
|
|
655
|
+
'senate',
|
|
656
|
+
'congress',
|
|
657
|
+
'government',
|
|
658
|
+
'parliament',
|
|
659
|
+
'campaign',
|
|
660
|
+
];
|
|
661
|
+
const BROWSE_ENTERTAINMENT_KEYWORDS = [
|
|
662
|
+
'entertain',
|
|
663
|
+
'movie',
|
|
664
|
+
'film',
|
|
665
|
+
'music',
|
|
666
|
+
'album',
|
|
667
|
+
'artist',
|
|
668
|
+
'tv',
|
|
669
|
+
'celebrity',
|
|
670
|
+
'oscar',
|
|
671
|
+
'emmy',
|
|
672
|
+
'grammy',
|
|
673
|
+
'box office',
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
function normalizeBrowseCategoryList(input) {
|
|
677
|
+
const values = Array.isArray(input) ? input : [input];
|
|
678
|
+
const normalized = [];
|
|
679
|
+
for (const value of values) {
|
|
680
|
+
const text = String(value || '').trim().toLowerCase();
|
|
681
|
+
if (!text || !BROWSE_ALLOWED_CATEGORIES.has(text)) continue;
|
|
682
|
+
normalized.push(text);
|
|
683
|
+
}
|
|
684
|
+
return Array.from(new Set(normalized));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeBrowseSortBy(value) {
|
|
688
|
+
const text = String(value || '').trim().toLowerCase();
|
|
689
|
+
if (!text || text === 'volume24h' || text === 'volume24husd' || text === 'volume') return 'volume24h';
|
|
690
|
+
if (text === 'liquidity' || text === 'liquidityusd') return 'liquidity';
|
|
691
|
+
if (text === 'enddate' || text === 'end-date' || text === 'close' || text === 'close-time' || text === 'closetimestamp') {
|
|
692
|
+
return 'endDate';
|
|
693
|
+
}
|
|
694
|
+
return 'volume24h';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function collectTagEntries(row) {
|
|
698
|
+
const entries = [];
|
|
699
|
+
const eventTags = row && Array.isArray(row.event_tags) ? row.event_tags : [];
|
|
700
|
+
const directTags = row && Array.isArray(row.tags) ? row.tags : [];
|
|
701
|
+
entries.push(...eventTags);
|
|
702
|
+
entries.push(...directTags);
|
|
703
|
+
if (row && row.tag_id !== undefined) entries.push({ id: row.tag_id });
|
|
704
|
+
if (row && row.tagId !== undefined) entries.push({ id: row.tagId });
|
|
705
|
+
return entries;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function readTagId(tag) {
|
|
709
|
+
if (tag === null || tag === undefined) return null;
|
|
710
|
+
if (typeof tag === 'number' && Number.isFinite(tag)) {
|
|
711
|
+
const asInt = Math.trunc(tag);
|
|
712
|
+
return asInt > 0 ? asInt : null;
|
|
713
|
+
}
|
|
714
|
+
if (typeof tag === 'string' && /^\d+$/.test(tag.trim())) {
|
|
715
|
+
const asInt = Math.trunc(Number(tag.trim()));
|
|
716
|
+
return asInt > 0 ? asInt : null;
|
|
717
|
+
}
|
|
718
|
+
if (typeof tag === 'object') {
|
|
719
|
+
const candidate = tag.id !== undefined ? tag.id : tag.tag_id !== undefined ? tag.tag_id : tag.tagId;
|
|
720
|
+
return readTagId(candidate);
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function readTagTextValues(tag) {
|
|
726
|
+
if (tag === null || tag === undefined) return [];
|
|
727
|
+
if (typeof tag === 'string') return [tag];
|
|
728
|
+
if (typeof tag === 'number') return [String(tag)];
|
|
729
|
+
if (typeof tag !== 'object') return [];
|
|
730
|
+
return [
|
|
731
|
+
tag.slug,
|
|
732
|
+
tag.name,
|
|
733
|
+
tag.label,
|
|
734
|
+
tag.title,
|
|
735
|
+
tag.group,
|
|
736
|
+
tag.topic,
|
|
737
|
+
tag.category,
|
|
738
|
+
tag.shortName,
|
|
739
|
+
tag.short_name,
|
|
740
|
+
]
|
|
741
|
+
.map((value) => String(value || '').trim())
|
|
742
|
+
.filter(Boolean);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function includesKeyword(textPool, keywords) {
|
|
746
|
+
return textPool.some((text) => keywords.some((keyword) => text.includes(keyword)));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function classifyBrowseCategories(item) {
|
|
750
|
+
const row = item && item.raw && typeof item.raw === 'object' ? item.raw : {};
|
|
751
|
+
const tagEntries = collectTagEntries(row);
|
|
752
|
+
const tagIds = [];
|
|
753
|
+
const tagText = [];
|
|
754
|
+
const seenText = new Set();
|
|
755
|
+
const seenTagIds = new Set();
|
|
756
|
+
|
|
757
|
+
for (const entry of tagEntries) {
|
|
758
|
+
const tagId = readTagId(entry);
|
|
759
|
+
if (tagId !== null && !seenTagIds.has(tagId)) {
|
|
760
|
+
seenTagIds.add(tagId);
|
|
761
|
+
tagIds.push(tagId);
|
|
762
|
+
}
|
|
763
|
+
for (const text of readTagTextValues(entry)) {
|
|
764
|
+
const normalized = normalizeText(text);
|
|
765
|
+
if (!normalized || seenText.has(normalized)) continue;
|
|
766
|
+
seenText.add(normalized);
|
|
767
|
+
tagText.push(normalized);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const textPool = tagText.concat(
|
|
772
|
+
[item && item.eventTitle, item && item.eventSlug, item && item.slug, item && item.question]
|
|
773
|
+
.map((value) => normalizeText(value))
|
|
774
|
+
.filter(Boolean),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
const categories = new Set();
|
|
778
|
+
if (tagIds.some((value) => BROWSE_DEFAULT_SPORT_TAG_IDS.includes(value)) || includesKeyword(textPool, BROWSE_SPORT_KEYWORDS)) {
|
|
779
|
+
categories.add('sports');
|
|
780
|
+
}
|
|
781
|
+
if (includesKeyword(textPool, BROWSE_CRYPTO_KEYWORDS)) {
|
|
782
|
+
categories.add('crypto');
|
|
783
|
+
}
|
|
784
|
+
if (includesKeyword(textPool, BROWSE_POLITICS_KEYWORDS)) {
|
|
785
|
+
categories.add('politics');
|
|
786
|
+
}
|
|
787
|
+
if (includesKeyword(textPool, BROWSE_ENTERTAINMENT_KEYWORDS)) {
|
|
788
|
+
categories.add('entertainment');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
categories: Array.from(categories),
|
|
793
|
+
tagIds,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
619
797
|
async function fetchGammaRowsByTagIds(params, options = {}, diagnostics = []) {
|
|
620
798
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
621
799
|
const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
|
|
@@ -1132,7 +1310,13 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1132
1310
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
1133
1311
|
const requestedLimit = Number.isInteger(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 10;
|
|
1134
1312
|
const scanLimit = Math.max(requestedLimit * 5, 100);
|
|
1135
|
-
const
|
|
1313
|
+
const requestedTagIds = normalizeTagIdList(options.polymarketTagIds);
|
|
1314
|
+
const categoryFilters = normalizeBrowseCategoryList(options.categories);
|
|
1315
|
+
const autoSportsTagIds = requestedTagIds.length === 0 && categoryFilters.includes('sports') ? [...BROWSE_DEFAULT_SPORT_TAG_IDS] : [];
|
|
1316
|
+
const polymarketTagIds = requestedTagIds.length ? requestedTagIds : autoSportsTagIds;
|
|
1317
|
+
if (autoSportsTagIds.length) {
|
|
1318
|
+
diagnostics.push(`No explicit sports tag ids provided; using defaults: ${autoSportsTagIds.join(', ')}.`);
|
|
1319
|
+
}
|
|
1136
1320
|
const useSportsEventsEndpoint = polymarketTagIds.length > 0;
|
|
1137
1321
|
|
|
1138
1322
|
let rows = [];
|
|
@@ -1172,20 +1356,86 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1172
1356
|
const closesAfter = toTimestampSeconds(options.closesAfter);
|
|
1173
1357
|
const closesBefore = toTimestampSeconds(options.closesBefore);
|
|
1174
1358
|
const questionContains = normalizeText(options.questionContains);
|
|
1359
|
+
const keyword = normalizeText(options.keyword);
|
|
1360
|
+
const slugContains = normalizeText(options.slug);
|
|
1361
|
+
const excludeSports = Boolean(options.excludeSports);
|
|
1362
|
+
const sortBy = normalizeBrowseSortBy(options.sortBy);
|
|
1175
1363
|
|
|
1176
1364
|
const normalized = rows.map((row) => normalizeMarketRow(row));
|
|
1177
|
-
const
|
|
1365
|
+
const enriched = normalized.map((item) => {
|
|
1366
|
+
const classification = classifyBrowseCategories(item);
|
|
1367
|
+
return {
|
|
1368
|
+
...item,
|
|
1369
|
+
categories: classification.categories,
|
|
1370
|
+
tagIds: classification.tagIds,
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
const filtered = enriched.filter((item) => {
|
|
1178
1375
|
if (item.active === false || item.resolved) return false;
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1376
|
+
const yesPct = toOptionalNumber(item.yesPct);
|
|
1377
|
+
if ((minYesPct !== null || maxYesPct !== null) && yesPct === null) return false;
|
|
1378
|
+
if (minYesPct !== null && yesPct < minYesPct) return false;
|
|
1379
|
+
if (maxYesPct !== null && yesPct > maxYesPct) return false;
|
|
1181
1380
|
if ((toOptionalNumber(item.volume24hUsd) || 0) < minVolume24h) return false;
|
|
1182
1381
|
if (closesAfter !== null && toIntegerOrNull(item.closeTimestamp) !== null && toIntegerOrNull(item.closeTimestamp) < closesAfter) return false;
|
|
1183
1382
|
if (closesBefore !== null && toIntegerOrNull(item.closeTimestamp) !== null && toIntegerOrNull(item.closeTimestamp) > closesBefore) return false;
|
|
1184
1383
|
if (questionContains && !normalizeText(item.question).includes(questionContains)) return false;
|
|
1384
|
+
if (slugContains) {
|
|
1385
|
+
const slugHaystack = [item.slug, item.eventSlug].map((value) => normalizeText(value)).filter(Boolean).join(' ');
|
|
1386
|
+
if (!slugHaystack.includes(slugContains)) return false;
|
|
1387
|
+
}
|
|
1388
|
+
if (keyword) {
|
|
1389
|
+
const keywordHaystack = [item.question, item.slug, item.eventSlug, item.eventTitle]
|
|
1390
|
+
.map((value) => normalizeText(value))
|
|
1391
|
+
.filter(Boolean)
|
|
1392
|
+
.join(' ');
|
|
1393
|
+
if (!keywordHaystack.includes(keyword)) return false;
|
|
1394
|
+
}
|
|
1395
|
+
if (excludeSports && Array.isArray(item.categories) && item.categories.includes('sports')) return false;
|
|
1396
|
+
if (categoryFilters.length) {
|
|
1397
|
+
const hasCategory = Array.isArray(item.categories) && item.categories.some((value) => categoryFilters.includes(value));
|
|
1398
|
+
if (!hasCategory) return false;
|
|
1399
|
+
}
|
|
1185
1400
|
return true;
|
|
1186
1401
|
});
|
|
1187
1402
|
|
|
1188
|
-
filtered.sort((left, right) =>
|
|
1403
|
+
filtered.sort((left, right) => {
|
|
1404
|
+
const leftVolume = toOptionalNumber(left.volume24hUsd) || 0;
|
|
1405
|
+
const rightVolume = toOptionalNumber(right.volume24hUsd) || 0;
|
|
1406
|
+
const leftLiquidity = toOptionalNumber(left.liquidityUsd) || 0;
|
|
1407
|
+
const rightLiquidity = toOptionalNumber(right.liquidityUsd) || 0;
|
|
1408
|
+
const leftClose = toIntegerOrNull(left.closeTimestamp);
|
|
1409
|
+
const rightClose = toIntegerOrNull(right.closeTimestamp);
|
|
1410
|
+
|
|
1411
|
+
if (sortBy === 'liquidity') {
|
|
1412
|
+
if (rightLiquidity !== leftLiquidity) return rightLiquidity - leftLiquidity;
|
|
1413
|
+
if (rightVolume !== leftVolume) return rightVolume - leftVolume;
|
|
1414
|
+
if (leftClose === null && rightClose === null) return 0;
|
|
1415
|
+
if (leftClose === null) return 1;
|
|
1416
|
+
if (rightClose === null) return -1;
|
|
1417
|
+
return leftClose - rightClose;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (sortBy === 'endDate') {
|
|
1421
|
+
if (leftClose === null && rightClose === null) {
|
|
1422
|
+
if (rightVolume !== leftVolume) return rightVolume - leftVolume;
|
|
1423
|
+
return rightLiquidity - leftLiquidity;
|
|
1424
|
+
}
|
|
1425
|
+
if (leftClose === null) return 1;
|
|
1426
|
+
if (rightClose === null) return -1;
|
|
1427
|
+
if (leftClose !== rightClose) return leftClose - rightClose;
|
|
1428
|
+
if (rightVolume !== leftVolume) return rightVolume - leftVolume;
|
|
1429
|
+
return rightLiquidity - leftLiquidity;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (rightVolume !== leftVolume) return rightVolume - leftVolume;
|
|
1433
|
+
if (rightLiquidity !== leftLiquidity) return rightLiquidity - leftLiquidity;
|
|
1434
|
+
if (leftClose === null && rightClose === null) return 0;
|
|
1435
|
+
if (leftClose === null) return 1;
|
|
1436
|
+
if (rightClose === null) return -1;
|
|
1437
|
+
return leftClose - rightClose;
|
|
1438
|
+
});
|
|
1189
1439
|
|
|
1190
1440
|
const items = filtered.slice(0, requestedLimit).map((item) => ({
|
|
1191
1441
|
marketId: item.marketId,
|
|
@@ -1201,6 +1451,7 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1201
1451
|
liquidityUsd: round(item.liquidityUsd, 6),
|
|
1202
1452
|
active: item.active,
|
|
1203
1453
|
resolved: item.resolved,
|
|
1454
|
+
categories: Array.isArray(item.categories) ? item.categories : [],
|
|
1204
1455
|
url: item.url,
|
|
1205
1456
|
sourceType: item.source || sourceType,
|
|
1206
1457
|
}));
|
|
@@ -1219,6 +1470,11 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1219
1470
|
closesAfter,
|
|
1220
1471
|
closesBefore,
|
|
1221
1472
|
questionContains: options.questionContains || null,
|
|
1473
|
+
keyword: options.keyword || null,
|
|
1474
|
+
slug: options.slug || null,
|
|
1475
|
+
categories: categoryFilters,
|
|
1476
|
+
excludeSports,
|
|
1477
|
+
sortBy,
|
|
1222
1478
|
limit: requestedLimit,
|
|
1223
1479
|
polymarketTagIds,
|
|
1224
1480
|
},
|
|
@@ -267,7 +267,7 @@ function buildCommandDescriptors() {
|
|
|
267
267
|
'mirror.browse': commandDescriptor({
|
|
268
268
|
summary: 'Browse Polymarket mirror candidates with optional sports tag filters.',
|
|
269
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>]',
|
|
270
|
+
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>|--end-date-after <date|72h>] [--closes-before <date>|--end-date-before <date|72h>] [--question-contains <text>|--keyword <text>] [--slug <text>] [--category sports|crypto|politics|entertainment] [--exclude-sports] [--sort-by volume24h|liquidity|endDate] [--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
271
|
emits: ['mirror.browse', 'mirror.browse.help'],
|
|
272
272
|
dataSchema: '#/definitions/MirrorBrowsePayload',
|
|
273
273
|
}),
|
package/cli/pandora.cjs
CHANGED
|
@@ -516,7 +516,11 @@ function getDoctorServiceInstance() {
|
|
|
516
516
|
const ROOT = path.resolve(__dirname, '..');
|
|
517
517
|
const DEFAULT_ENV_FILE_PRIMARY = path.join(ROOT, 'scripts', '.env');
|
|
518
518
|
const DEFAULT_ENV_FILE_FALLBACK = path.join(os.homedir(), '.pandora-cli.env');
|
|
519
|
-
const DEFAULT_ENV_FILE = fs.existsSync(
|
|
519
|
+
const DEFAULT_ENV_FILE = fs.existsSync(DEFAULT_ENV_FILE_FALLBACK)
|
|
520
|
+
? DEFAULT_ENV_FILE_FALLBACK
|
|
521
|
+
: fs.existsSync(DEFAULT_ENV_FILE_PRIMARY)
|
|
522
|
+
? DEFAULT_ENV_FILE_PRIMARY
|
|
523
|
+
: DEFAULT_ENV_FILE_FALLBACK;
|
|
520
524
|
const DEFAULT_ENV_EXAMPLE = path.join(ROOT, 'scripts', '.env.example');
|
|
521
525
|
const DEFAULT_INDEXER_URL = SHARED_DEFAULT_INDEXER_URL;
|
|
522
526
|
let PACKAGE_VERSION = '0.0.0';
|
|
@@ -855,7 +859,7 @@ Examples:
|
|
|
855
859
|
|
|
856
860
|
Notes:
|
|
857
861
|
- launch/clone-bet forward unknown flags directly to underlying scripts.
|
|
858
|
-
- Env auto-load default:
|
|
862
|
+
- Env auto-load default: ~/.pandora-cli.env when present; otherwise scripts/.env. Use --skip-dotenv to disable.
|
|
859
863
|
- --output json is supported for all commands except launch/clone-bet.
|
|
860
864
|
- Indexer URL resolution order: --indexer-url, PANDORA_INDEXER_URL, INDEXER_URL, default public indexer.
|
|
861
865
|
- mirror status --with-live can enrich output with Polymarket position data when POLYMARKET_* credentials are set; missing endpoints/creds return diagnostics instead of hard failures.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pandora-cli-skills",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.52",
|
|
4
4
|
"description": "Pandora CLI & Skills",
|
|
5
5
|
"main": "cli/pandora.cjs",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"init-env": "node cli/pandora.cjs init-env",
|
|
30
30
|
"doctor": "node cli/pandora.cjs doctor",
|
|
31
31
|
"setup": "node cli/pandora.cjs setup",
|
|
32
|
+
"analytics:mega": "node analytics/dune/mega/server.cjs",
|
|
32
33
|
"dry-run": "node cli/pandora.cjs launch --dry-run",
|
|
33
34
|
"execute": "node cli/pandora.cjs launch --execute",
|
|
34
35
|
"dry-run:clone": "node cli/pandora.cjs clone-bet --dry-run",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"dependencies": {
|
|
69
70
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
70
71
|
"@polymarket/clob-client": "^5.2.4",
|
|
72
|
+
"playwright-core": "^1.58.2",
|
|
71
73
|
"tsx": "^4.21.0",
|
|
72
74
|
"viem": "^2.46.2",
|
|
73
75
|
"ws": "^8.19.0"
|
|
@@ -5024,6 +5024,126 @@ test('mirror browse supports sports tag filters via gamma events endpoint', asyn
|
|
|
5024
5024
|
}
|
|
5025
5025
|
});
|
|
5026
5026
|
|
|
5027
|
+
test('mirror browse supports non-sports short-window filtering in one call', async () => {
|
|
5028
|
+
const indexer = await startIndexerMockServer(buildMirrorIndexerOverrides());
|
|
5029
|
+
const nowMs = Date.now();
|
|
5030
|
+
const toIso = (offsetHours) => new Date(nowMs + offsetHours * 60 * 60 * 1000).toISOString();
|
|
5031
|
+
const gamma = await startJsonHttpServer((request) => {
|
|
5032
|
+
const parsed = new URL(request.url || '/', 'http://127.0.0.1');
|
|
5033
|
+
if (parsed.pathname !== '/markets') {
|
|
5034
|
+
return { status: 404, body: { error: 'not found' } };
|
|
5035
|
+
}
|
|
5036
|
+
return {
|
|
5037
|
+
body: {
|
|
5038
|
+
markets: [
|
|
5039
|
+
{
|
|
5040
|
+
condition_id: 's1',
|
|
5041
|
+
market_slug: 'everton-v-burnley-home',
|
|
5042
|
+
question: 'Will Everton beat Burnley?',
|
|
5043
|
+
end_date_iso: toIso(24),
|
|
5044
|
+
active: true,
|
|
5045
|
+
closed: false,
|
|
5046
|
+
volume24hr: 9000,
|
|
5047
|
+
liquidity: 9000,
|
|
5048
|
+
tags: [{ id: 82, slug: 'soccer' }],
|
|
5049
|
+
tokens: [
|
|
5050
|
+
{ outcome: 'Yes', price: '0.61', token_id: 's1-yes' },
|
|
5051
|
+
{ outcome: 'No', price: '0.39', token_id: 's1-no' },
|
|
5052
|
+
],
|
|
5053
|
+
},
|
|
5054
|
+
{
|
|
5055
|
+
condition_id: 'c1',
|
|
5056
|
+
market_slug: 'bitcoin-etf-approval-2026',
|
|
5057
|
+
question: 'Will bitcoin ETF approval happen in 2026?',
|
|
5058
|
+
end_date_iso: toIso(36),
|
|
5059
|
+
active: true,
|
|
5060
|
+
closed: false,
|
|
5061
|
+
volume24hr: 8000,
|
|
5062
|
+
liquidity: 1000,
|
|
5063
|
+
tags: [{ slug: 'crypto' }],
|
|
5064
|
+
tokens: [
|
|
5065
|
+
{ outcome: 'Yes', price: '0.45', token_id: 'c1-yes' },
|
|
5066
|
+
{ outcome: 'No', price: '0.55', token_id: 'c1-no' },
|
|
5067
|
+
],
|
|
5068
|
+
},
|
|
5069
|
+
{
|
|
5070
|
+
condition_id: 'c2',
|
|
5071
|
+
market_slug: 'bitcoin-price-120k-2026',
|
|
5072
|
+
question: 'Will bitcoin trade above 120k in 2026?',
|
|
5073
|
+
end_date_iso: toIso(18),
|
|
5074
|
+
active: true,
|
|
5075
|
+
closed: false,
|
|
5076
|
+
volume24hr: 2000,
|
|
5077
|
+
liquidity: 7000,
|
|
5078
|
+
tags: [{ slug: 'crypto' }],
|
|
5079
|
+
tokens: [
|
|
5080
|
+
{ outcome: 'Yes', price: '0.55', token_id: 'c2-yes' },
|
|
5081
|
+
{ outcome: 'No', price: '0.45', token_id: 'c2-no' },
|
|
5082
|
+
],
|
|
5083
|
+
},
|
|
5084
|
+
{
|
|
5085
|
+
condition_id: 'x1',
|
|
5086
|
+
market_slug: 'bitcoin-over-300k',
|
|
5087
|
+
question: 'Will bitcoin exceed 300k?',
|
|
5088
|
+
end_date_iso: toIso(12),
|
|
5089
|
+
active: true,
|
|
5090
|
+
closed: false,
|
|
5091
|
+
volume24hr: 10000,
|
|
5092
|
+
liquidity: 1000,
|
|
5093
|
+
tags: [{ slug: 'crypto' }],
|
|
5094
|
+
tokens: [
|
|
5095
|
+
{ outcome: 'Yes', price: '0.95', token_id: 'x1-yes' },
|
|
5096
|
+
{ outcome: 'No', price: '0.05', token_id: 'x1-no' },
|
|
5097
|
+
],
|
|
5098
|
+
},
|
|
5099
|
+
],
|
|
5100
|
+
},
|
|
5101
|
+
};
|
|
5102
|
+
});
|
|
5103
|
+
|
|
5104
|
+
try {
|
|
5105
|
+
const result = await runCliAsync([
|
|
5106
|
+
'--output',
|
|
5107
|
+
'json',
|
|
5108
|
+
'mirror',
|
|
5109
|
+
'browse',
|
|
5110
|
+
'--skip-dotenv',
|
|
5111
|
+
'--indexer-url',
|
|
5112
|
+
indexer.url,
|
|
5113
|
+
'--polymarket-gamma-url',
|
|
5114
|
+
gamma.url,
|
|
5115
|
+
'--exclude-sports',
|
|
5116
|
+
'--end-date-before',
|
|
5117
|
+
'72h',
|
|
5118
|
+
'--min-yes-pct',
|
|
5119
|
+
'15',
|
|
5120
|
+
'--max-yes-pct',
|
|
5121
|
+
'85',
|
|
5122
|
+
'--sort-by',
|
|
5123
|
+
'volume24h',
|
|
5124
|
+
'--keyword',
|
|
5125
|
+
'bitcoin',
|
|
5126
|
+
'--limit',
|
|
5127
|
+
'10',
|
|
5128
|
+
]);
|
|
5129
|
+
|
|
5130
|
+
assert.equal(result.status, 0);
|
|
5131
|
+
const payload = parseJsonOutput(result);
|
|
5132
|
+
assert.equal(payload.ok, true);
|
|
5133
|
+
assert.equal(payload.command, 'mirror.browse');
|
|
5134
|
+
assert.equal(payload.data.filters.excludeSports, true);
|
|
5135
|
+
assert.equal(payload.data.filters.sortBy, 'volume24h');
|
|
5136
|
+
assert.equal(payload.data.count, 2);
|
|
5137
|
+
assert.equal(payload.data.items[0].slug, 'bitcoin-etf-approval-2026');
|
|
5138
|
+
assert.equal(payload.data.items[1].slug, 'bitcoin-price-120k-2026');
|
|
5139
|
+
assert.ok(Array.isArray(payload.data.items[0].categories));
|
|
5140
|
+
assert.ok(payload.data.items[0].categories.includes('crypto'));
|
|
5141
|
+
} finally {
|
|
5142
|
+
await indexer.close();
|
|
5143
|
+
await gamma.close();
|
|
5144
|
+
}
|
|
5145
|
+
});
|
|
5146
|
+
|
|
5027
5147
|
test('mirror sync accepts --market-address with --dry-run mode alias', async () => {
|
|
5028
5148
|
const tempDir = createTempDir('pandora-mirror-sync-aliases-');
|
|
5029
5149
|
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
@@ -53,6 +53,7 @@ const { createParseWatchFlags } = require('../../cli/lib/parsers/watch_flags.cjs
|
|
|
53
53
|
const { createParseAutopilotFlags } = require('../../cli/lib/parsers/autopilot_flags.cjs');
|
|
54
54
|
const { createParseMirrorDeployFlags } = require('../../cli/lib/parsers/mirror_deploy_flags.cjs');
|
|
55
55
|
const { createParseMirrorGoFlags } = require('../../cli/lib/parsers/mirror_go_flags.cjs');
|
|
56
|
+
const { createParseMirrorBrowseFlags } = require('../../cli/lib/parsers/mirror_remaining_flags.cjs');
|
|
56
57
|
const { createParseLifecycleFlags } = require('../../cli/lib/parsers/lifecycle_flags.cjs');
|
|
57
58
|
const { createParseOddsFlags } = require('../../cli/lib/parsers/odds_flags.cjs');
|
|
58
59
|
const {
|
|
@@ -165,6 +166,19 @@ function parserIsSecureHttpUrlOrLocal(value) {
|
|
|
165
166
|
return /^https:\/\//.test(value) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(value);
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
function parserParseDateLikeFlag(value, flagName) {
|
|
170
|
+
const text = String(value || '').trim();
|
|
171
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) {
|
|
172
|
+
throw new ParserCliError('INVALID_FLAG_VALUE', `${flagName} must be an ISO date/time string.`);
|
|
173
|
+
}
|
|
174
|
+
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(text) ? `${text}T00:00:00Z` : text;
|
|
175
|
+
const parsed = Date.parse(normalized);
|
|
176
|
+
if (!Number.isFinite(parsed)) {
|
|
177
|
+
throw new ParserCliError('INVALID_FLAG_VALUE', `${flagName} must be an ISO date/time string.`);
|
|
178
|
+
}
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
function parserParseMirrorSyncGateSkipList(value, flagName) {
|
|
169
183
|
const checks = String(value)
|
|
170
184
|
.split(',')
|
|
@@ -1152,6 +1166,149 @@ test('browsePolymarketMarkets uses gamma events endpoint for tag-id sports disco
|
|
|
1152
1166
|
}
|
|
1153
1167
|
});
|
|
1154
1168
|
|
|
1169
|
+
test('browsePolymarketMarkets supports category/keyword/date filters with explicit sorting', async () => {
|
|
1170
|
+
const nowMs = Date.now();
|
|
1171
|
+
const toIso = (offsetHours) => new Date(nowMs + offsetHours * 60 * 60 * 1000).toISOString();
|
|
1172
|
+
|
|
1173
|
+
const server = http.createServer((req, res) => {
|
|
1174
|
+
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
1175
|
+
res.statusCode = 200;
|
|
1176
|
+
res.setHeader('content-type', 'application/json');
|
|
1177
|
+
if (parsed.pathname !== '/markets') {
|
|
1178
|
+
res.end(JSON.stringify({ markets: [] }));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
res.end(
|
|
1183
|
+
JSON.stringify({
|
|
1184
|
+
markets: [
|
|
1185
|
+
{
|
|
1186
|
+
condition_id: 'm-sports',
|
|
1187
|
+
market_slug: 'everton-v-burnley-home',
|
|
1188
|
+
question: 'Will Everton beat Burnley?',
|
|
1189
|
+
end_date_iso: toIso(12),
|
|
1190
|
+
active: true,
|
|
1191
|
+
closed: false,
|
|
1192
|
+
volume24hr: 9000,
|
|
1193
|
+
liquidity: 9000,
|
|
1194
|
+
tags: [{ id: 82, slug: 'soccer' }],
|
|
1195
|
+
tokens: [
|
|
1196
|
+
{ outcome: 'Yes', price: '0.60', token_id: 'sports-yes' },
|
|
1197
|
+
{ outcome: 'No', price: '0.40', token_id: 'sports-no' },
|
|
1198
|
+
],
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
condition_id: 'm-crypto-high-vol',
|
|
1202
|
+
market_slug: 'bitcoin-etf-approval-2026',
|
|
1203
|
+
question: 'Will Bitcoin ETF approval happen in 2026?',
|
|
1204
|
+
end_date_iso: toIso(24),
|
|
1205
|
+
active: true,
|
|
1206
|
+
closed: false,
|
|
1207
|
+
volume24hr: 8000,
|
|
1208
|
+
liquidity: 1000,
|
|
1209
|
+
tags: [{ slug: 'crypto' }],
|
|
1210
|
+
tokens: [
|
|
1211
|
+
{ outcome: 'Yes', price: '0.45', token_id: 'c1-yes' },
|
|
1212
|
+
{ outcome: 'No', price: '0.55', token_id: 'c1-no' },
|
|
1213
|
+
],
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
condition_id: 'm-crypto-high-liq',
|
|
1217
|
+
market_slug: 'bitcoin-price-120k-2026',
|
|
1218
|
+
question: 'Will bitcoin trade above 120k in 2026?',
|
|
1219
|
+
end_date_iso: toIso(18),
|
|
1220
|
+
active: true,
|
|
1221
|
+
closed: false,
|
|
1222
|
+
volume24hr: 2000,
|
|
1223
|
+
liquidity: 7000,
|
|
1224
|
+
tags: [{ slug: 'crypto' }],
|
|
1225
|
+
tokens: [
|
|
1226
|
+
{ outcome: 'Yes', price: '0.55', token_id: 'c2-yes' },
|
|
1227
|
+
{ outcome: 'No', price: '0.45', token_id: 'c2-no' },
|
|
1228
|
+
],
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
condition_id: 'm-crypto-extreme',
|
|
1232
|
+
market_slug: 'bitcoin-over-300k',
|
|
1233
|
+
question: 'Will bitcoin exceed 300k?',
|
|
1234
|
+
end_date_iso: toIso(10),
|
|
1235
|
+
active: true,
|
|
1236
|
+
closed: false,
|
|
1237
|
+
volume24hr: 10000,
|
|
1238
|
+
liquidity: 1000,
|
|
1239
|
+
tags: [{ slug: 'crypto' }],
|
|
1240
|
+
tokens: [
|
|
1241
|
+
{ outcome: 'Yes', price: '0.95', token_id: 'c3-yes' },
|
|
1242
|
+
{ outcome: 'No', price: '0.05', token_id: 'c3-no' },
|
|
1243
|
+
],
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
condition_id: 'm-crypto-far',
|
|
1247
|
+
market_slug: 'bitcoin-long-dated',
|
|
1248
|
+
question: 'Will bitcoin close above 200k by 2028?',
|
|
1249
|
+
end_date_iso: toIso(120),
|
|
1250
|
+
active: true,
|
|
1251
|
+
closed: false,
|
|
1252
|
+
volume24hr: 11000,
|
|
1253
|
+
liquidity: 11000,
|
|
1254
|
+
tags: [{ slug: 'crypto' }],
|
|
1255
|
+
tokens: [
|
|
1256
|
+
{ outcome: 'Yes', price: '0.50', token_id: 'c4-yes' },
|
|
1257
|
+
{ outcome: 'No', price: '0.50', token_id: 'c4-no' },
|
|
1258
|
+
],
|
|
1259
|
+
},
|
|
1260
|
+
],
|
|
1261
|
+
}),
|
|
1262
|
+
);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1266
|
+
const { port } = server.address();
|
|
1267
|
+
const gammaUrl = `http://127.0.0.1:${port}`;
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
const byVolume = await browsePolymarketMarkets({
|
|
1271
|
+
gammaUrl,
|
|
1272
|
+
minYesPct: 15,
|
|
1273
|
+
maxYesPct: 85,
|
|
1274
|
+
closesBefore: toIso(72),
|
|
1275
|
+
keyword: 'bitcoin',
|
|
1276
|
+
categories: ['crypto'],
|
|
1277
|
+
excludeSports: true,
|
|
1278
|
+
sortBy: 'volume24h',
|
|
1279
|
+
limit: 10,
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
assert.equal(byVolume.count, 2);
|
|
1283
|
+
assert.equal(byVolume.items[0].slug, 'bitcoin-etf-approval-2026');
|
|
1284
|
+
assert.equal(byVolume.items[1].slug, 'bitcoin-price-120k-2026');
|
|
1285
|
+
assert.ok(Array.isArray(byVolume.items[0].categories));
|
|
1286
|
+
assert.ok(byVolume.items[0].categories.includes('crypto'));
|
|
1287
|
+
assert.equal(byVolume.filters.excludeSports, true);
|
|
1288
|
+
assert.deepEqual(byVolume.filters.categories, ['crypto']);
|
|
1289
|
+
assert.equal(byVolume.filters.sortBy, 'volume24h');
|
|
1290
|
+
|
|
1291
|
+
const byLiquidity = await browsePolymarketMarkets({
|
|
1292
|
+
gammaUrl,
|
|
1293
|
+
minYesPct: 15,
|
|
1294
|
+
maxYesPct: 85,
|
|
1295
|
+
closesBefore: toIso(72),
|
|
1296
|
+
keyword: 'bitcoin',
|
|
1297
|
+
categories: ['crypto'],
|
|
1298
|
+
excludeSports: true,
|
|
1299
|
+
sortBy: 'liquidity',
|
|
1300
|
+
limit: 10,
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
assert.equal(byLiquidity.count, 2);
|
|
1304
|
+
assert.equal(byLiquidity.items[0].slug, 'bitcoin-price-120k-2026');
|
|
1305
|
+
assert.equal(byLiquidity.items[1].slug, 'bitcoin-etf-approval-2026');
|
|
1306
|
+
assert.equal(byLiquidity.filters.sortBy, 'liquidity');
|
|
1307
|
+
} finally {
|
|
1308
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1155
1312
|
test('computeApprovalDiff deterministically marks missing allowance/operator checks', () => {
|
|
1156
1313
|
const payload = computeApprovalDiff({
|
|
1157
1314
|
ownerAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
@@ -1493,6 +1650,165 @@ test('runMirrorSync handles thrown hedgeFn errors without consuming idempotency'
|
|
|
1493
1650
|
}
|
|
1494
1651
|
});
|
|
1495
1652
|
|
|
1653
|
+
test('runMirrorSync run mode continues after transient tick verification failures', async () => {
|
|
1654
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-tick-retry-'));
|
|
1655
|
+
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
1656
|
+
const killSwitchFile = path.join(tempDir, 'STOP');
|
|
1657
|
+
|
|
1658
|
+
const verifyPayload = {
|
|
1659
|
+
matchConfidence: 0.99,
|
|
1660
|
+
gateResult: {
|
|
1661
|
+
ok: true,
|
|
1662
|
+
failedChecks: [],
|
|
1663
|
+
checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
|
|
1664
|
+
},
|
|
1665
|
+
sourceMarket: {
|
|
1666
|
+
source: 'polymarket',
|
|
1667
|
+
marketId: 'poly-cond-1',
|
|
1668
|
+
yesPct: 60,
|
|
1669
|
+
yesTokenId: 'yes-token',
|
|
1670
|
+
noTokenId: 'no-token',
|
|
1671
|
+
},
|
|
1672
|
+
pandora: {
|
|
1673
|
+
yesPct: 55,
|
|
1674
|
+
reserveYes: 5,
|
|
1675
|
+
reserveNo: 5,
|
|
1676
|
+
},
|
|
1677
|
+
expiry: { minTimeToExpirySec: 7200 },
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
let verifyCallCount = 0;
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
const payload = await runMirrorSync(
|
|
1684
|
+
{
|
|
1685
|
+
mode: 'run',
|
|
1686
|
+
iterations: 3,
|
|
1687
|
+
indexerUrl: 'https://example.invalid/graphql',
|
|
1688
|
+
timeoutMs: 1000,
|
|
1689
|
+
pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
1690
|
+
polymarketMarketId: 'poly-cond-1',
|
|
1691
|
+
executeLive: false,
|
|
1692
|
+
trustDeploy: false,
|
|
1693
|
+
hedgeEnabled: false,
|
|
1694
|
+
hedgeRatio: 1,
|
|
1695
|
+
intervalMs: 1,
|
|
1696
|
+
driftTriggerBps: 10000,
|
|
1697
|
+
hedgeTriggerUsdc: 1000,
|
|
1698
|
+
maxRebalanceUsdc: 25,
|
|
1699
|
+
maxHedgeUsdc: 10,
|
|
1700
|
+
maxOpenExposureUsdc: 100,
|
|
1701
|
+
maxTradesPerDay: 10,
|
|
1702
|
+
cooldownMs: 1000,
|
|
1703
|
+
depthSlippageBps: 100,
|
|
1704
|
+
stateFile,
|
|
1705
|
+
killSwitchFile,
|
|
1706
|
+
polymarketHost: 'https://clob.polymarket.com',
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
verifyFn: async () => {
|
|
1710
|
+
verifyCallCount += 1;
|
|
1711
|
+
if (verifyCallCount === 2) {
|
|
1712
|
+
const error = new Error('temporary indexer timeout');
|
|
1713
|
+
error.code = 'INDEXER_TIMEOUT';
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1716
|
+
return verifyPayload;
|
|
1717
|
+
},
|
|
1718
|
+
depthFn: async () => ({
|
|
1719
|
+
depthWithinSlippageUsd: 1000,
|
|
1720
|
+
yesDepth: { depthUsd: 1000, midPrice: 0.4, worstPrice: 0.41 },
|
|
1721
|
+
noDepth: { depthUsd: 1000, midPrice: 0.6, worstPrice: 0.61 },
|
|
1722
|
+
}),
|
|
1723
|
+
sleep: async () => {},
|
|
1724
|
+
},
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
assert.equal(payload.iterationsCompleted, 3);
|
|
1728
|
+
assert.equal(payload.snapshots.length, 3);
|
|
1729
|
+
assert.equal(payload.diagnostics.length, 1);
|
|
1730
|
+
assert.equal(payload.diagnostics[0].code, 'INDEXER_TIMEOUT');
|
|
1731
|
+
assert.equal(payload.diagnostics[0].scope, 'tick');
|
|
1732
|
+
assert.equal(payload.snapshots[1].action.status, 'error');
|
|
1733
|
+
assert.equal(payload.snapshots[1].error.code, 'INDEXER_TIMEOUT');
|
|
1734
|
+
} finally {
|
|
1735
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
test('runMirrorSync once mode still fails fast on tick errors', async () => {
|
|
1740
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-once-fail-'));
|
|
1741
|
+
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
1742
|
+
const killSwitchFile = path.join(tempDir, 'STOP');
|
|
1743
|
+
|
|
1744
|
+
const verifyPayload = {
|
|
1745
|
+
matchConfidence: 0.99,
|
|
1746
|
+
gateResult: {
|
|
1747
|
+
ok: true,
|
|
1748
|
+
failedChecks: [],
|
|
1749
|
+
checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
|
|
1750
|
+
},
|
|
1751
|
+
sourceMarket: {
|
|
1752
|
+
source: 'polymarket',
|
|
1753
|
+
marketId: 'poly-cond-1',
|
|
1754
|
+
yesPct: 60,
|
|
1755
|
+
yesTokenId: 'yes-token',
|
|
1756
|
+
noTokenId: 'no-token',
|
|
1757
|
+
},
|
|
1758
|
+
pandora: {
|
|
1759
|
+
yesPct: 55,
|
|
1760
|
+
reserveYes: 5,
|
|
1761
|
+
reserveNo: 5,
|
|
1762
|
+
},
|
|
1763
|
+
expiry: { minTimeToExpirySec: 7200 },
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
try {
|
|
1767
|
+
await assert.rejects(
|
|
1768
|
+
runMirrorSync(
|
|
1769
|
+
{
|
|
1770
|
+
mode: 'once',
|
|
1771
|
+
indexerUrl: 'https://example.invalid/graphql',
|
|
1772
|
+
timeoutMs: 1000,
|
|
1773
|
+
pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
1774
|
+
polymarketMarketId: 'poly-cond-1',
|
|
1775
|
+
executeLive: false,
|
|
1776
|
+
trustDeploy: false,
|
|
1777
|
+
hedgeEnabled: false,
|
|
1778
|
+
hedgeRatio: 1,
|
|
1779
|
+
intervalMs: 1,
|
|
1780
|
+
driftTriggerBps: 10000,
|
|
1781
|
+
hedgeTriggerUsdc: 1000,
|
|
1782
|
+
maxRebalanceUsdc: 25,
|
|
1783
|
+
maxHedgeUsdc: 10,
|
|
1784
|
+
maxOpenExposureUsdc: 100,
|
|
1785
|
+
maxTradesPerDay: 10,
|
|
1786
|
+
cooldownMs: 1000,
|
|
1787
|
+
depthSlippageBps: 100,
|
|
1788
|
+
stateFile,
|
|
1789
|
+
killSwitchFile,
|
|
1790
|
+
polymarketHost: 'https://clob.polymarket.com',
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
verifyFn: async () => verifyPayload,
|
|
1794
|
+
depthFn: async () => {
|
|
1795
|
+
const error = new Error('depth fetch unavailable');
|
|
1796
|
+
error.code = 'DEPTH_FETCH_FAILED';
|
|
1797
|
+
throw error;
|
|
1798
|
+
},
|
|
1799
|
+
sleep: async () => {},
|
|
1800
|
+
},
|
|
1801
|
+
),
|
|
1802
|
+
(error) => {
|
|
1803
|
+
assert.equal(error.code, 'DEPTH_FETCH_FAILED');
|
|
1804
|
+
return true;
|
|
1805
|
+
},
|
|
1806
|
+
);
|
|
1807
|
+
} finally {
|
|
1808
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1496
1812
|
test('runAutopilot does not consume budget/idempotency when executeFn throws', async () => {
|
|
1497
1813
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-autopilot-failure-'));
|
|
1498
1814
|
const stateFile = path.join(tempDir, 'autopilot-state.json');
|
|
@@ -1623,6 +1939,59 @@ test('createParseTradeFlags enforces secure --rpc-url and drops dead funder fiel
|
|
|
1623
1939
|
);
|
|
1624
1940
|
});
|
|
1625
1941
|
|
|
1942
|
+
test('createParseMirrorBrowseFlags parses relative end-date windows and new browse selectors', () => {
|
|
1943
|
+
const parseMirrorBrowseFlags = createParseMirrorBrowseFlags(
|
|
1944
|
+
buildParserDeps({
|
|
1945
|
+
parseDateLikeFlag: parserParseDateLikeFlag,
|
|
1946
|
+
}),
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
const startedAt = Date.now();
|
|
1950
|
+
const options = parseMirrorBrowseFlags([
|
|
1951
|
+
'--end-date-after',
|
|
1952
|
+
'1h',
|
|
1953
|
+
'--end-date-before',
|
|
1954
|
+
'72h',
|
|
1955
|
+
'--keyword',
|
|
1956
|
+
'bitcoin',
|
|
1957
|
+
'--slug',
|
|
1958
|
+
'etf',
|
|
1959
|
+
'--category',
|
|
1960
|
+
'crypto,politics',
|
|
1961
|
+
'--sort-by',
|
|
1962
|
+
'liquidity',
|
|
1963
|
+
'--limit',
|
|
1964
|
+
'7',
|
|
1965
|
+
]);
|
|
1966
|
+
|
|
1967
|
+
assert.equal(options.keyword, 'bitcoin');
|
|
1968
|
+
assert.equal(options.slug, 'etf');
|
|
1969
|
+
assert.deepEqual(options.categories, ['crypto', 'politics']);
|
|
1970
|
+
assert.equal(options.sortBy, 'liquidity');
|
|
1971
|
+
assert.equal(options.limit, 7);
|
|
1972
|
+
assert.ok(Number.isFinite(Date.parse(options.closesAfter)));
|
|
1973
|
+
assert.ok(Number.isFinite(Date.parse(options.closesBefore)));
|
|
1974
|
+
assert.ok(Date.parse(options.closesAfter) >= startedAt + 45 * 60 * 1000);
|
|
1975
|
+
assert.ok(Date.parse(options.closesBefore) >= startedAt + 70 * 60 * 60 * 1000);
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
test('createParseMirrorBrowseFlags rejects contradictory sports filters', () => {
|
|
1979
|
+
const parseMirrorBrowseFlags = createParseMirrorBrowseFlags(
|
|
1980
|
+
buildParserDeps({
|
|
1981
|
+
parseDateLikeFlag: parserParseDateLikeFlag,
|
|
1982
|
+
}),
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
assert.throws(
|
|
1986
|
+
() => parseMirrorBrowseFlags(['--category', 'sports', '--exclude-sports']),
|
|
1987
|
+
(error) => {
|
|
1988
|
+
assert.equal(error.code, 'INVALID_ARGS');
|
|
1989
|
+
assert.match(error.message, /cannot be combined/i);
|
|
1990
|
+
return true;
|
|
1991
|
+
},
|
|
1992
|
+
);
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1626
1995
|
test('resolveForkRuntime supports attach-only fork mode with strict env/flag precedence', () => {
|
|
1627
1996
|
const live = resolveForkRuntime(
|
|
1628
1997
|
{ chainId: 146 },
|