pandora-cli-skills 1.1.9 → 1.1.10

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.
@@ -35,13 +35,14 @@ Prerequisite: Node.js `>=18`.
35
35
  - `ORACLE`
36
36
  - `FACTORY`
37
37
  - `USDC`
38
- - optional for live mirror hedging:
38
+ - optional for live mirror hedging and `mirror status --with-live` position diagnostics:
39
39
  - `POLYMARKET_PRIVATE_KEY`
40
- - `POLYMARKET_FUNDER`
40
+ - `POLYMARKET_FUNDER` (Polymarket proxy wallet / Gnosis Safe address, not your EOA)
41
41
  - `POLYMARKET_API_KEY`
42
42
  - `POLYMARKET_API_SECRET`
43
43
  - `POLYMARKET_API_PASSPHRASE`
44
44
  - `POLYMARKET_HOST`
45
+ - note: live Polymarket trading settles with Polygon USDC.e collateral on the proxy wallet.
45
46
  4. Validate and build:
46
47
  - `npm run doctor`
47
48
  - `npm run build`
@@ -208,6 +209,8 @@ Prerequisite: Node.js `>=18`.
208
209
  - Polymarket resilience: when Polymarket endpoints are unreachable, cached snapshots under `~/.pandora/polymarket` are reused for read paths; live sync blocks execution if source data is cached/stale.
209
210
  - `mirror status`:
210
211
  - envelope is `ok=true`, `command="mirror.status"`, with `data.stateFile`, `data.strategyHash`, persisted `data.state`, and optional `data.live` when `--with-live` is used.
212
+ - `data.live` now includes additive position diagnostics: `polymarketPosition.{yesBalance,noBalance,openOrdersCount,estimatedValueUsd,diagnostics[]}` plus `netDeltaApprox` and `pnlApprox`.
213
+ - if Polymarket credentials/endpoints are unavailable, `--with-live` remains non-fatal and returns diagnostics with null position fields.
211
214
  - `mirror close`:
212
215
  - envelope is `ok=true`, `command="mirror.close"`, with `data.mode` and unwind `data.steps[]` scaffold.
213
216
  - `webhook test`:
package/SKILL.md CHANGED
@@ -171,9 +171,13 @@ pandora --output json suggest --wallet <0x...> --risk medium --budget 50 --inclu
171
171
  - `mirror go`: one-command orchestration for plan → deploy → verify, with optional auto-sync start.
172
172
  - `mirror sync`: paper-first delta-neutral loop with strict gates, state persistence, and optional live hedging (`--hedge-ratio <n>`, `--no-hedge`).
173
173
  - live hedge env: `POLYMARKET_PRIVATE_KEY`, `POLYMARKET_FUNDER`, `POLYMARKET_API_KEY`, `POLYMARKET_API_SECRET`, `POLYMARKET_API_PASSPHRASE`, `POLYMARKET_HOST`.
174
+ - `POLYMARKET_FUNDER` must be the Polymarket proxy wallet (Gnosis Safe), not the EOA address.
175
+ - Polymarket trading collateral is Polygon USDC.e on the proxy wallet.
174
176
  - rebalance sizing is pool-aware and bounded by `--max-rebalance-usdc`.
175
177
  - endpoint resilience: Polymarket snapshots are cached under `~/.pandora/polymarket`; paper/read flows can reuse cache during outages, while live sync blocks cached sources.
176
178
  - `mirror status`: local mirror state inspection with optional live market diagnostics (`--with-live`).
179
+ - `--with-live` uses the same `POLYMARKET_*` env keys for optional position visibility (YES/NO balances, open orders count, estimated value) and adds `netDeltaApprox` / `pnlApprox`.
180
+ - missing credentials or unavailable position endpoints do not hard-fail status; diagnostics are returned with null position fields.
177
181
  - `mirror close`: deterministic unwind scaffold for LP withdrawal + hedge unwind flow.
178
182
  - `webhook test`: channel validation for generic, Telegram, and Discord payload delivery.
179
183
  - `leaderboard`: ranked user aggregates by profit/volume/win-rate.
@@ -0,0 +1,268 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { spawn } = require('child_process');
6
+ const { expandHome } = require('./mirror_state_store.cjs');
7
+
8
+ const MIRROR_DAEMON_SCHEMA_VERSION = '1.0.0';
9
+ const STOP_TIMEOUT_MS = 5_000;
10
+ const STOP_POLL_MS = 100;
11
+
12
+ function createServiceError(code, message, details = undefined) {
13
+ const err = new Error(message);
14
+ err.code = code;
15
+ if (details !== undefined) {
16
+ err.details = details;
17
+ }
18
+ return err;
19
+ }
20
+
21
+ function normalizeStrategyHash(strategyHash) {
22
+ const value = String(strategyHash || '').trim().toLowerCase();
23
+ if (!/^[a-f0-9]{16}$/.test(value)) {
24
+ throw createServiceError('INVALID_FLAG_VALUE', '--strategy-hash must be a 16-character hex value.');
25
+ }
26
+ return value;
27
+ }
28
+
29
+ function resolvePath(filePath) {
30
+ return path.resolve(expandHome(String(filePath || '').trim()));
31
+ }
32
+
33
+ function defaultPidFile(strategyHash) {
34
+ const hash = normalizeStrategyHash(strategyHash);
35
+ return path.join(os.homedir(), '.pandora', 'mirror', 'daemon', `${hash}.json`);
36
+ }
37
+
38
+ function defaultLogFile(strategyHash) {
39
+ const hash = normalizeStrategyHash(strategyHash);
40
+ return path.join(os.homedir(), '.pandora', 'mirror', 'logs', `${hash}.log`);
41
+ }
42
+
43
+ function writeJsonFile(filePath, payload) {
44
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`;
46
+ const serialized = JSON.stringify(payload, null, 2);
47
+ fs.writeFileSync(tmpPath, serialized);
48
+ fs.renameSync(tmpPath, filePath);
49
+ }
50
+
51
+ function readJsonFile(filePath) {
52
+ if (!fs.existsSync(filePath)) return null;
53
+ try {
54
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
55
+ } catch (err) {
56
+ throw createServiceError('MIRROR_DAEMON_PIDFILE_INVALID', `Failed to parse daemon pid file at ${filePath}.`, {
57
+ cause: err && err.message ? err.message : String(err),
58
+ });
59
+ }
60
+ }
61
+
62
+ function isPidAlive(pid) {
63
+ const numericPid = Number(pid);
64
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
65
+ try {
66
+ process.kill(numericPid, 0);
67
+ return true;
68
+ } catch (err) {
69
+ if (err && (err.code === 'ESRCH' || err.code === 'ENOENT')) return false;
70
+ if (err && err.code === 'EPERM') return true;
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ function sleep(ms) {
76
+ return new Promise((resolve) => setTimeout(resolve, ms));
77
+ }
78
+
79
+ async function waitForProcessExit(pid, timeoutMs = STOP_TIMEOUT_MS) {
80
+ const startedAt = Date.now();
81
+ while (Date.now() - startedAt < timeoutMs) {
82
+ if (!isPidAlive(pid)) return true;
83
+ await sleep(STOP_POLL_MS);
84
+ }
85
+ return !isPidAlive(pid);
86
+ }
87
+
88
+ function resolvePidFile(options = {}) {
89
+ if (options.pidFile) {
90
+ return resolvePath(options.pidFile);
91
+ }
92
+ if (options.strategyHash) {
93
+ return defaultPidFile(options.strategyHash);
94
+ }
95
+ throw createServiceError(
96
+ 'MISSING_REQUIRED_FLAG',
97
+ 'mirror sync daemon lifecycle requires --pid-file <path> or --strategy-hash <hash>.',
98
+ );
99
+ }
100
+
101
+ function startDaemon(options = {}) {
102
+ const strategyHash = normalizeStrategyHash(options.strategyHash);
103
+ const pidFile = defaultPidFile(strategyHash);
104
+ const logFile = options.logFile ? resolvePath(options.logFile) : defaultLogFile(strategyHash);
105
+ const cliPath = options.cliPath ? resolvePath(options.cliPath) : null;
106
+ const cliArgs = Array.isArray(options.cliArgs) ? options.cliArgs.map((item) => String(item)) : [];
107
+
108
+ if (!cliPath) {
109
+ throw createServiceError('MIRROR_DAEMON_CLI_PATH_REQUIRED', 'Daemon start requires the CLI path.');
110
+ }
111
+ if (!cliArgs.length) {
112
+ throw createServiceError('MIRROR_DAEMON_CLI_ARGS_REQUIRED', 'Daemon start requires sync run CLI arguments.');
113
+ }
114
+
115
+ const existing = readJsonFile(pidFile);
116
+ if (existing && isPidAlive(existing.pid)) {
117
+ throw createServiceError('MIRROR_DAEMON_ALREADY_RUNNING', 'Mirror sync daemon is already running for this strategy.', {
118
+ pidFile,
119
+ pid: existing.pid,
120
+ strategyHash,
121
+ });
122
+ }
123
+
124
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
125
+ const logFd = fs.openSync(logFile, 'a');
126
+ const child = spawn(process.execPath, [cliPath, ...cliArgs], {
127
+ cwd: options.cwd || process.cwd(),
128
+ env: options.env || process.env,
129
+ detached: true,
130
+ stdio: ['ignore', logFd, logFd],
131
+ });
132
+ child.unref();
133
+ fs.closeSync(logFd);
134
+
135
+ const metadata = {
136
+ schemaVersion: MIRROR_DAEMON_SCHEMA_VERSION,
137
+ strategyHash,
138
+ pid: child.pid,
139
+ pidAlive: isPidAlive(child.pid),
140
+ startedAt: new Date().toISOString(),
141
+ checkedAt: new Date().toISOString(),
142
+ status: isPidAlive(child.pid) ? 'running' : 'unknown',
143
+ pidFile,
144
+ logFile,
145
+ cliPath,
146
+ cliArgs,
147
+ stateFile: options.stateFile || null,
148
+ killSwitchFile: options.killSwitchFile || null,
149
+ mode: options.mode || 'run',
150
+ executeLive: Boolean(options.executeLive),
151
+ pandoraMarketAddress: options.pandoraMarketAddress || null,
152
+ polymarketMarketId: options.polymarketMarketId || null,
153
+ polymarketSlug: options.polymarketSlug || null,
154
+ launchCommand: [process.execPath, cliPath, ...cliArgs].join(' '),
155
+ };
156
+ writeJsonFile(pidFile, metadata);
157
+
158
+ return metadata;
159
+ }
160
+
161
+ async function stopDaemon(options = {}) {
162
+ const pidFile = resolvePidFile(options);
163
+ const metadata = readJsonFile(pidFile);
164
+ if (!metadata) {
165
+ throw createServiceError('MIRROR_DAEMON_NOT_FOUND', `No daemon metadata found at ${pidFile}.`, { pidFile });
166
+ }
167
+
168
+ const pid = Number(metadata.pid);
169
+ const wasAlive = isPidAlive(pid);
170
+ let signalSent = false;
171
+ if (wasAlive) {
172
+ process.kill(pid, 'SIGTERM');
173
+ signalSent = true;
174
+ }
175
+
176
+ let exited = wasAlive ? await waitForProcessExit(pid, options.stopTimeoutMs || STOP_TIMEOUT_MS) : true;
177
+ let alive = isPidAlive(pid);
178
+ let forceKilled = false;
179
+ if (wasAlive && alive) {
180
+ try {
181
+ process.kill(pid, 'SIGKILL');
182
+ forceKilled = true;
183
+ exited = await waitForProcessExit(pid, 2_000);
184
+ alive = isPidAlive(pid);
185
+ } catch (err) {
186
+ if (!(err && (err.code === 'ESRCH' || err.code === 'ENOENT'))) {
187
+ throw err;
188
+ }
189
+ exited = true;
190
+ alive = false;
191
+ }
192
+ }
193
+
194
+ const updated = {
195
+ ...metadata,
196
+ checkedAt: new Date().toISOString(),
197
+ pidAlive: alive,
198
+ status: alive ? 'running' : 'stopped',
199
+ stopAttemptedAt: new Date().toISOString(),
200
+ stopSignal: signalSent ? 'SIGTERM' : null,
201
+ stopForceSignal: forceKilled ? 'SIGKILL' : null,
202
+ stopSignalSent: signalSent,
203
+ stopExitObserved: exited,
204
+ stopForceKilled: forceKilled,
205
+ };
206
+ writeJsonFile(pidFile, updated);
207
+
208
+ return {
209
+ schemaVersion: MIRROR_DAEMON_SCHEMA_VERSION,
210
+ strategyHash: updated.strategyHash || null,
211
+ pidFile,
212
+ pid,
213
+ wasAlive,
214
+ signalSent,
215
+ forceKilled,
216
+ exitObserved: exited,
217
+ alive,
218
+ status: updated.status,
219
+ metadata: updated,
220
+ };
221
+ }
222
+
223
+ function daemonStatus(options = {}) {
224
+ const pidFile = resolvePidFile(options);
225
+ const metadata = readJsonFile(pidFile);
226
+
227
+ if (!metadata) {
228
+ return {
229
+ schemaVersion: MIRROR_DAEMON_SCHEMA_VERSION,
230
+ found: false,
231
+ pidFile,
232
+ strategyHash: options.strategyHash ? normalizeStrategyHash(options.strategyHash) : null,
233
+ pid: null,
234
+ alive: false,
235
+ status: 'not-found',
236
+ metadata: null,
237
+ };
238
+ }
239
+
240
+ const pid = Number(metadata.pid);
241
+ const alive = isPidAlive(pid);
242
+ const updated = {
243
+ ...metadata,
244
+ checkedAt: new Date().toISOString(),
245
+ pidAlive: alive,
246
+ status: alive ? 'running' : 'stopped',
247
+ };
248
+ writeJsonFile(pidFile, updated);
249
+
250
+ return {
251
+ schemaVersion: MIRROR_DAEMON_SCHEMA_VERSION,
252
+ found: true,
253
+ pidFile,
254
+ strategyHash: updated.strategyHash || null,
255
+ pid,
256
+ alive,
257
+ status: updated.status,
258
+ metadata: updated,
259
+ };
260
+ }
261
+
262
+ module.exports = {
263
+ MIRROR_DAEMON_SCHEMA_VERSION,
264
+ defaultPidFile,
265
+ startDaemon,
266
+ stopDaemon,
267
+ daemonStatus,
268
+ };
@@ -410,6 +410,8 @@ async function runMirrorSync(options, deps = {}) {
410
410
  rebalance: null,
411
411
  hedge: null,
412
412
  };
413
+ let actualRebalanceUsdc = 0;
414
+ let actualHedgeUsdc = 0;
413
415
  state.idempotencyKeys.push(idempotencyKey);
414
416
  pruneIdempotencyKeys(state);
415
417
  state.lastExecution = {
@@ -439,6 +441,7 @@ async function runMirrorSync(options, deps = {}) {
439
441
  result: { status: 'simulated' },
440
442
  };
441
443
  }
444
+ actualRebalanceUsdc = plannedRebalanceUsdc;
442
445
 
443
446
  state.cumulativeLpFeesApproxUsdc =
444
447
  round((toNumber(state.cumulativeLpFeesApproxUsdc) || 0) + plannedRebalanceUsdc * 0.003, 6) || 0;
@@ -450,18 +453,18 @@ async function runMirrorSync(options, deps = {}) {
450
453
  const hedgeDepth = gapUsdc >= 0 ? depth.yesDepth : depth.noDepth;
451
454
 
452
455
  if (options.executeLive) {
453
- const creds = readTradingCredsFromEnv();
456
+ const envCreds = readTradingCredsFromEnv();
454
457
  const hedgeResult = await hedgeFn({
455
458
  host: options.polymarketHost,
456
459
  mockUrl: options.polymarketMockUrl,
457
460
  tokenId,
458
461
  side: hedgeSide,
459
462
  amountUsd: plannedHedgeUsdc,
460
- privateKey: creds.privateKey,
461
- funder: creds.funder,
462
- apiKey: creds.apiKey,
463
- apiSecret: creds.apiSecret,
464
- apiPassphrase: creds.apiPassphrase,
463
+ privateKey: options.privateKey || envCreds.privateKey,
464
+ funder: options.funder || envCreds.funder,
465
+ apiKey: envCreds.apiKey,
466
+ apiSecret: envCreds.apiSecret,
467
+ apiPassphrase: envCreds.apiPassphrase,
465
468
  });
466
469
  action.hedge = {
467
470
  tokenId,
@@ -477,21 +480,39 @@ async function runMirrorSync(options, deps = {}) {
477
480
  result: { status: 'simulated' },
478
481
  };
479
482
  }
480
-
481
- const direction = gapUsdc >= 0 ? 1 : -1;
482
- state.currentHedgeUsdc = round((toNumber(state.currentHedgeUsdc) || 0) + direction * plannedHedgeUsdc, 6) || 0;
483
- state.cumulativeHedgeNotionalUsdc =
484
- round((toNumber(state.cumulativeHedgeNotionalUsdc) || 0) + plannedHedgeUsdc, 6) || 0;
485
- const slippageRatio =
486
- hedgeDepth && hedgeDepth.midPrice !== null && hedgeDepth.worstPrice !== null && hedgeDepth.midPrice > 0
487
- ? Math.max(0, Math.abs(hedgeDepth.worstPrice - hedgeDepth.midPrice) / hedgeDepth.midPrice)
488
- : 0;
489
- const hedgeCostApprox = plannedHedgeUsdc * slippageRatio;
490
- state.cumulativeHedgeCostApproxUsdc =
491
- round((toNumber(state.cumulativeHedgeCostApproxUsdc) || 0) + hedgeCostApprox, 6) || 0;
483
+ const hedgeResultOk = !options.executeLive || (action.hedge && action.hedge.result && action.hedge.result.ok !== false);
484
+ if (hedgeResultOk) {
485
+ actualHedgeUsdc = plannedHedgeUsdc;
486
+ const direction = gapUsdc >= 0 ? 1 : -1;
487
+ state.currentHedgeUsdc =
488
+ round((toNumber(state.currentHedgeUsdc) || 0) + direction * plannedHedgeUsdc, 6) || 0;
489
+ state.cumulativeHedgeNotionalUsdc =
490
+ round((toNumber(state.cumulativeHedgeNotionalUsdc) || 0) + plannedHedgeUsdc, 6) || 0;
491
+ const slippageRatio =
492
+ hedgeDepth && hedgeDepth.midPrice !== null && hedgeDepth.worstPrice !== null && hedgeDepth.midPrice > 0
493
+ ? Math.max(0, Math.abs(hedgeDepth.worstPrice - hedgeDepth.midPrice) / hedgeDepth.midPrice)
494
+ : 0;
495
+ const hedgeCostApprox = plannedHedgeUsdc * slippageRatio;
496
+ state.cumulativeHedgeCostApproxUsdc =
497
+ round((toNumber(state.cumulativeHedgeCostApproxUsdc) || 0) + hedgeCostApprox, 6) || 0;
498
+ } else {
499
+ action.status = 'failed';
500
+ const hedgeError =
501
+ action.hedge && action.hedge.result && action.hedge.result.error
502
+ ? action.hedge.result.error
503
+ : action.hedge && action.hedge.result && action.hedge.result.response && action.hedge.result.response.error
504
+ ? { message: String(action.hedge.result.response.error) }
505
+ : { message: 'Polymarket hedge execution failed.' };
506
+ action.error = {
507
+ code: 'HEDGE_EXECUTION_FAILED',
508
+ message: hedgeError.message || 'Polymarket hedge execution failed.',
509
+ details: hedgeError,
510
+ };
511
+ }
492
512
  }
493
513
 
494
- state.dailySpendUsdc = round((toNumber(state.dailySpendUsdc) || 0) + plannedSpendUsdc, 6) || 0;
514
+ const actualSpendUsdc = round(actualRebalanceUsdc + actualHedgeUsdc, 6) || 0;
515
+ state.dailySpendUsdc = round((toNumber(state.dailySpendUsdc) || 0) + actualSpendUsdc, 6) || 0;
495
516
  const executedLegCount = (action.rebalance ? 1 : 0) + (action.hedge ? 1 : 0);
496
517
  state.tradesToday += executedLegCount || 1;
497
518
  state.lastExecution = action;