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.
- package/README_FOR_SHARING.md +5 -2
- package/SKILL.md +4 -0
- package/cli/lib/mirror_daemon_service.cjs +268 -0
- package/cli/lib/mirror_sync_service.cjs +40 -19
- package/cli/lib/polymarket_ops_service.cjs +831 -0
- package/cli/lib/polymarket_trade_adapter.cjs +511 -30
- package/cli/pandora.cjs +680 -9
- package/package.json +1 -1
- package/scripts/.env.example +1 -0
package/README_FOR_SHARING.md
CHANGED
|
@@ -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
|
|
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:
|
|
461
|
-
funder:
|
|
462
|
-
apiKey:
|
|
463
|
-
apiSecret:
|
|
464
|
-
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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;
|