pandora-cli-skills 1.1.31 → 1.1.32
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/cli/lib/mirror_sync/execution.cjs +331 -0
- package/cli/lib/mirror_sync/gates.cjs +282 -0
- package/cli/lib/mirror_sync/planning.cjs +167 -0
- package/cli/lib/mirror_sync/state.cjs +48 -0
- package/cli/lib/mirror_sync_service.cjs +34 -796
- package/cli/lib/parsers/mirror_deploy_flags.cjs +9 -2
- package/cli/lib/parsers/trade_flags.cjs +9 -2
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const { saveState, pruneIdempotencyKeys } = require('../mirror_state_store.cjs');
|
|
2
|
+
const { readTradingCredsFromEnv } = require('../polymarket_trade_adapter.cjs');
|
|
3
|
+
const { toNumber, round } = require('../shared/utils.cjs');
|
|
4
|
+
|
|
5
|
+
function buildIdempotencyKey(options, snapshot, nowMs) {
|
|
6
|
+
const bucketSize = Math.max(1_000, Number(options.cooldownMs) || 60_000);
|
|
7
|
+
const bucket = Math.floor(nowMs / bucketSize);
|
|
8
|
+
const metrics = snapshot && snapshot.metrics ? snapshot.metrics : {};
|
|
9
|
+
const actionPlan = snapshot && snapshot.actionPlan ? snapshot.actionPlan : {};
|
|
10
|
+
const rebalanceUsdc = Math.round((toNumber(actionPlan.rebalanceUsdc) || 0) * 100) / 100;
|
|
11
|
+
const hedgeUsdc = Math.round((toNumber(actionPlan.hedgeUsdc) || 0) * 100) / 100;
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
String(options.pandoraMarketAddress || '').toLowerCase(),
|
|
15
|
+
String(options.polymarketMarketId || options.polymarketSlug || '').toLowerCase(),
|
|
16
|
+
metrics.driftTriggered ? 'drift' : 'no-drift',
|
|
17
|
+
metrics.hedgeTriggered ? 'hedge' : 'no-hedge',
|
|
18
|
+
actionPlan.rebalanceSide || 'rebalance:none',
|
|
19
|
+
actionPlan.hedgeTokenSide || 'hedge:none',
|
|
20
|
+
String(rebalanceUsdc),
|
|
21
|
+
String(hedgeUsdc),
|
|
22
|
+
String(bucket),
|
|
23
|
+
].join('|');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create an executable action envelope for a triggered tick.
|
|
28
|
+
* @param {{options: object, idempotencyKey: string, gate: object}} params
|
|
29
|
+
* @returns {object}
|
|
30
|
+
*/
|
|
31
|
+
function buildExecutableAction(params) {
|
|
32
|
+
const { options, idempotencyKey, gate } = params;
|
|
33
|
+
return {
|
|
34
|
+
mode: options.executeLive ? 'live' : 'paper',
|
|
35
|
+
status: options.executeLive ? 'executed' : 'simulated',
|
|
36
|
+
idempotencyKey,
|
|
37
|
+
forcedGateBypass: gate.bypassedFailedChecks.length > 0,
|
|
38
|
+
failedChecks: gate.failedChecks,
|
|
39
|
+
failedChecksRaw: gate.failedChecksRaw,
|
|
40
|
+
bypassedFailedChecks: gate.bypassedFailedChecks,
|
|
41
|
+
rebalance: null,
|
|
42
|
+
hedge: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize thrown execution errors into service result payloads.
|
|
48
|
+
* @param {any} err
|
|
49
|
+
* @returns {{ok: false, error: {code: string|null, message: string}}}
|
|
50
|
+
*/
|
|
51
|
+
function normalizeExecutionFailure(err) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: {
|
|
55
|
+
code: err && err.code ? String(err.code) : null,
|
|
56
|
+
message: err && err.message ? String(err.message) : String(err),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute or simulate rebalance leg and mutate action/state accordingly.
|
|
63
|
+
* @param {{options: object, action: object, plan: object, snapshotMetrics: object, rebalanceFn: Function, state: object}} params
|
|
64
|
+
* @returns {Promise<number>} Executed rebalance notional in USDC.
|
|
65
|
+
*/
|
|
66
|
+
async function executeRebalanceLeg(params) {
|
|
67
|
+
const { options, action, plan, snapshotMetrics, rebalanceFn, state } = params;
|
|
68
|
+
if (!(snapshotMetrics.driftTriggered && plan.plannedRebalanceUsdc > 0)) return 0;
|
|
69
|
+
|
|
70
|
+
let rebalanceResultOk = true;
|
|
71
|
+
if (options.executeLive) {
|
|
72
|
+
let rebalanceResult;
|
|
73
|
+
try {
|
|
74
|
+
rebalanceResult = await rebalanceFn({
|
|
75
|
+
marketAddress: options.pandoraMarketAddress,
|
|
76
|
+
side: plan.rebalanceSide,
|
|
77
|
+
amountUsdc: plan.plannedRebalanceUsdc,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
rebalanceResult = normalizeExecutionFailure(err);
|
|
81
|
+
}
|
|
82
|
+
action.rebalance = {
|
|
83
|
+
side: plan.rebalanceSide,
|
|
84
|
+
amountUsdc: plan.plannedRebalanceUsdc,
|
|
85
|
+
result: rebalanceResult,
|
|
86
|
+
};
|
|
87
|
+
rebalanceResultOk = Boolean(rebalanceResult && rebalanceResult.ok !== false);
|
|
88
|
+
} else {
|
|
89
|
+
action.rebalance = {
|
|
90
|
+
side: plan.rebalanceSide,
|
|
91
|
+
amountUsdc: plan.plannedRebalanceUsdc,
|
|
92
|
+
result: { status: 'simulated' },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (rebalanceResultOk) {
|
|
97
|
+
state.cumulativeLpFeesApproxUsdc =
|
|
98
|
+
round((toNumber(state.cumulativeLpFeesApproxUsdc) || 0) + plan.plannedRebalanceUsdc * 0.003, 6) || 0;
|
|
99
|
+
return plan.plannedRebalanceUsdc;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
action.status = 'failed';
|
|
103
|
+
const rebalanceError =
|
|
104
|
+
action.rebalance && action.rebalance.result && action.rebalance.result.error
|
|
105
|
+
? action.rebalance.result.error
|
|
106
|
+
: { message: 'Pandora rebalance execution failed.' };
|
|
107
|
+
action.error = {
|
|
108
|
+
code: 'REBALANCE_EXECUTION_FAILED',
|
|
109
|
+
message: rebalanceError.message || 'Pandora rebalance execution failed.',
|
|
110
|
+
details: rebalanceError,
|
|
111
|
+
};
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute or simulate hedge leg and mutate action/state accordingly.
|
|
117
|
+
* @param {{options: object, action: object, plan: object, verifyPayload: object, depth: object, hedgeFn: Function, state: object}} params
|
|
118
|
+
* @returns {Promise<number>} Executed hedge notional in USDC.
|
|
119
|
+
*/
|
|
120
|
+
async function executeHedgeLeg(params) {
|
|
121
|
+
const { options, action, plan, verifyPayload, depth, hedgeFn, state } = params;
|
|
122
|
+
if (!(plan.hedgeTriggered && plan.plannedHedgeUsdc > 0)) return 0;
|
|
123
|
+
|
|
124
|
+
const hedgeSide = 'buy';
|
|
125
|
+
const tokenId = plan.gapUsdc >= 0 ? verifyPayload.sourceMarket.yesTokenId : verifyPayload.sourceMarket.noTokenId;
|
|
126
|
+
const hedgeDepth = plan.gapUsdc >= 0 ? depth.yesDepth : depth.noDepth;
|
|
127
|
+
|
|
128
|
+
if (options.executeLive) {
|
|
129
|
+
const envCreds = readTradingCredsFromEnv();
|
|
130
|
+
let hedgeResult;
|
|
131
|
+
if (!options.privateKey && envCreds.privateKeyInvalid) {
|
|
132
|
+
hedgeResult = {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: {
|
|
135
|
+
code: 'INVALID_ENV',
|
|
136
|
+
message: 'POLYMARKET_PRIVATE_KEY must be a valid private key (0x + 64 hex chars).',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
hedgeResult = await hedgeFn({
|
|
142
|
+
host: options.polymarketHost,
|
|
143
|
+
mockUrl: options.polymarketMockUrl,
|
|
144
|
+
tokenId,
|
|
145
|
+
side: hedgeSide,
|
|
146
|
+
amountUsd: plan.plannedHedgeUsdc,
|
|
147
|
+
privateKey: options.privateKey || envCreds.privateKey,
|
|
148
|
+
funder: options.funder || envCreds.funder,
|
|
149
|
+
apiKey: envCreds.apiKey,
|
|
150
|
+
apiSecret: envCreds.apiSecret,
|
|
151
|
+
apiPassphrase: envCreds.apiPassphrase,
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
hedgeResult = normalizeExecutionFailure(err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
action.hedge = {
|
|
158
|
+
tokenId,
|
|
159
|
+
side: hedgeSide,
|
|
160
|
+
amountUsdc: plan.plannedHedgeUsdc,
|
|
161
|
+
result: hedgeResult,
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
action.hedge = {
|
|
165
|
+
tokenId,
|
|
166
|
+
side: hedgeSide,
|
|
167
|
+
amountUsdc: plan.plannedHedgeUsdc,
|
|
168
|
+
result: { status: 'simulated' },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hedgeResultOk = !options.executeLive || (action.hedge && action.hedge.result && action.hedge.result.ok !== false);
|
|
173
|
+
if (hedgeResultOk) {
|
|
174
|
+
const direction = plan.gapUsdc >= 0 ? 1 : -1;
|
|
175
|
+
state.currentHedgeUsdc =
|
|
176
|
+
round((toNumber(state.currentHedgeUsdc) || 0) + direction * plan.plannedHedgeUsdc, 6) || 0;
|
|
177
|
+
state.cumulativeHedgeNotionalUsdc =
|
|
178
|
+
round((toNumber(state.cumulativeHedgeNotionalUsdc) || 0) + plan.plannedHedgeUsdc, 6) || 0;
|
|
179
|
+
const slippageRatio =
|
|
180
|
+
hedgeDepth && hedgeDepth.midPrice !== null && hedgeDepth.worstPrice !== null && hedgeDepth.midPrice > 0
|
|
181
|
+
? Math.max(0, Math.abs(hedgeDepth.worstPrice - hedgeDepth.midPrice) / hedgeDepth.midPrice)
|
|
182
|
+
: 0;
|
|
183
|
+
const hedgeCostApprox = plan.plannedHedgeUsdc * slippageRatio;
|
|
184
|
+
state.cumulativeHedgeCostApproxUsdc =
|
|
185
|
+
round((toNumber(state.cumulativeHedgeCostApproxUsdc) || 0) + hedgeCostApprox, 6) || 0;
|
|
186
|
+
return plan.plannedHedgeUsdc;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
action.status = 'failed';
|
|
190
|
+
const hedgeError =
|
|
191
|
+
action.hedge && action.hedge.result && action.hedge.result.error
|
|
192
|
+
? action.hedge.result.error
|
|
193
|
+
: action.hedge && action.hedge.result && action.hedge.result.response && action.hedge.result.response.error
|
|
194
|
+
? { message: String(action.hedge.result.response.error) }
|
|
195
|
+
: { message: 'Polymarket hedge execution failed.' };
|
|
196
|
+
action.error = {
|
|
197
|
+
code: 'HEDGE_EXECUTION_FAILED',
|
|
198
|
+
message: hedgeError.message || 'Polymarket hedge execution failed.',
|
|
199
|
+
details: hedgeError,
|
|
200
|
+
};
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Apply executed leg totals to persistent state.
|
|
206
|
+
* @param {{state: object, action: object, idempotencyKey: string, actualRebalanceUsdc: number, actualHedgeUsdc: number}} params
|
|
207
|
+
* @returns {void}
|
|
208
|
+
*/
|
|
209
|
+
function finalizeExecutedActionState(params) {
|
|
210
|
+
const { state, action, idempotencyKey, actualRebalanceUsdc, actualHedgeUsdc } = params;
|
|
211
|
+
const actualSpendUsdc = round(actualRebalanceUsdc + actualHedgeUsdc, 6) || 0;
|
|
212
|
+
state.dailySpendUsdc = round((toNumber(state.dailySpendUsdc) || 0) + actualSpendUsdc, 6) || 0;
|
|
213
|
+
const executedLegCount = (actualRebalanceUsdc > 0 ? 1 : 0) + (actualHedgeUsdc > 0 ? 1 : 0);
|
|
214
|
+
if (executedLegCount > 0) {
|
|
215
|
+
state.idempotencyKeys.push(idempotencyKey);
|
|
216
|
+
pruneIdempotencyKeys(state);
|
|
217
|
+
}
|
|
218
|
+
state.tradesToday += executedLegCount;
|
|
219
|
+
state.lastExecution = action;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Execute triggered action path (skip/blocked/executed) for a tick.
|
|
224
|
+
* @param {{options: object, state: object, snapshot: object, plan: object, gate: object, tickAt: Date, loadedFilePath: string, rebalanceFn: Function, hedgeFn: Function, sendWebhook: Function|null, strategyHash: string, iteration: number, actions: Array<object>, webhookReports: Array<object>, snapshotMetrics: object, verifyPayload: object, depth: object}} params
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
async function processTriggeredAction(params) {
|
|
228
|
+
const {
|
|
229
|
+
options,
|
|
230
|
+
state,
|
|
231
|
+
snapshot,
|
|
232
|
+
plan,
|
|
233
|
+
gate,
|
|
234
|
+
tickAt,
|
|
235
|
+
loadedFilePath,
|
|
236
|
+
rebalanceFn,
|
|
237
|
+
hedgeFn,
|
|
238
|
+
sendWebhook,
|
|
239
|
+
strategyHash: hash,
|
|
240
|
+
iteration,
|
|
241
|
+
actions,
|
|
242
|
+
webhookReports,
|
|
243
|
+
snapshotMetrics,
|
|
244
|
+
verifyPayload,
|
|
245
|
+
depth,
|
|
246
|
+
} = params;
|
|
247
|
+
|
|
248
|
+
const idempotencyKey = buildIdempotencyKey(options, snapshot, tickAt.getTime());
|
|
249
|
+
if ((state.idempotencyKeys || []).includes(idempotencyKey)) {
|
|
250
|
+
snapshot.action = {
|
|
251
|
+
mode: options.executeLive ? 'live' : 'paper',
|
|
252
|
+
status: 'skipped',
|
|
253
|
+
reason: 'Duplicate trigger bucket (idempotency key already processed).',
|
|
254
|
+
idempotencyKey,
|
|
255
|
+
};
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!gate.ok) {
|
|
260
|
+
snapshot.action = {
|
|
261
|
+
mode: options.executeLive ? 'live' : 'paper',
|
|
262
|
+
status: 'blocked',
|
|
263
|
+
reason: 'Strict gate blocked execution.',
|
|
264
|
+
idempotencyKey,
|
|
265
|
+
failedChecks: gate.failedChecks,
|
|
266
|
+
failedChecksRaw: gate.failedChecksRaw,
|
|
267
|
+
bypassedFailedChecks: gate.bypassedFailedChecks,
|
|
268
|
+
};
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const action = buildExecutableAction({ options, idempotencyKey, gate });
|
|
273
|
+
state.lastExecution = {
|
|
274
|
+
mode: action.mode,
|
|
275
|
+
status: 'pending',
|
|
276
|
+
idempotencyKey,
|
|
277
|
+
startedAt: tickAt.toISOString(),
|
|
278
|
+
};
|
|
279
|
+
saveState(loadedFilePath, state);
|
|
280
|
+
|
|
281
|
+
const actualRebalanceUsdc = await executeRebalanceLeg({
|
|
282
|
+
options,
|
|
283
|
+
action,
|
|
284
|
+
plan,
|
|
285
|
+
snapshotMetrics,
|
|
286
|
+
rebalanceFn,
|
|
287
|
+
state,
|
|
288
|
+
});
|
|
289
|
+
const actualHedgeUsdc = await executeHedgeLeg({
|
|
290
|
+
options,
|
|
291
|
+
action,
|
|
292
|
+
plan,
|
|
293
|
+
verifyPayload,
|
|
294
|
+
depth,
|
|
295
|
+
hedgeFn,
|
|
296
|
+
state,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
finalizeExecutedActionState({
|
|
300
|
+
state,
|
|
301
|
+
action,
|
|
302
|
+
idempotencyKey,
|
|
303
|
+
actualRebalanceUsdc,
|
|
304
|
+
actualHedgeUsdc,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
snapshot.action = action;
|
|
308
|
+
actions.push(action);
|
|
309
|
+
|
|
310
|
+
if (sendWebhook) {
|
|
311
|
+
const report = await sendWebhook({
|
|
312
|
+
event: 'mirror.sync.trigger',
|
|
313
|
+
strategyHash: hash,
|
|
314
|
+
iteration,
|
|
315
|
+
message: `[Pandora Mirror] action=${action.status} drift=${snapshotMetrics.driftBps} hedgeGap=${plan.gapUsdc}`,
|
|
316
|
+
action,
|
|
317
|
+
snapshot,
|
|
318
|
+
});
|
|
319
|
+
webhookReports.push(report);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = {
|
|
324
|
+
buildIdempotencyKey,
|
|
325
|
+
buildExecutableAction,
|
|
326
|
+
normalizeExecutionFailure,
|
|
327
|
+
executeRebalanceLeg,
|
|
328
|
+
executeHedgeLeg,
|
|
329
|
+
finalizeExecutedActionState,
|
|
330
|
+
processTriggeredAction,
|
|
331
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const { toNumber, round } = require('../shared/utils.cjs');
|
|
2
|
+
|
|
3
|
+
const MIRROR_SYNC_GATE_CODES = Object.freeze([
|
|
4
|
+
'MATCH_AND_RULES',
|
|
5
|
+
'POLYMARKET_SOURCE_FRESH',
|
|
6
|
+
'CLOSE_TIME_DELTA',
|
|
7
|
+
'DEPTH_COVERAGE',
|
|
8
|
+
'MAX_OPEN_EXPOSURE',
|
|
9
|
+
'MAX_TRADES_PER_DAY',
|
|
10
|
+
'MIN_TIME_TO_EXPIRY',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read min-time-to-expiry from verify payload in seconds.
|
|
15
|
+
* @param {object} verifyPayload
|
|
16
|
+
* @returns {number|null}
|
|
17
|
+
*/
|
|
18
|
+
function readMinTimeToExpirySec(verifyPayload) {
|
|
19
|
+
return verifyPayload && verifyPayload.expiry && Number.isFinite(Number(verifyPayload.expiry.minTimeToExpirySec))
|
|
20
|
+
? Number(verifyPayload.expiry.minTimeToExpirySec)
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive sync metrics from verification payloads.
|
|
26
|
+
* `driftBps` is in basis points; reserve/delta/hedge fields are decimal USDC.
|
|
27
|
+
* @param {object} verifyPayload
|
|
28
|
+
* @param {{driftTriggerBps: number}} options
|
|
29
|
+
* @returns {{
|
|
30
|
+
* sourceYesPct: number|null,
|
|
31
|
+
* pandoraYesPct: number|null,
|
|
32
|
+
* driftBps: number|null,
|
|
33
|
+
* driftTriggered: boolean,
|
|
34
|
+
* reserveYesUsdc: number|null,
|
|
35
|
+
* reserveNoUsdc: number|null,
|
|
36
|
+
* reserveTotalUsdc: number|null,
|
|
37
|
+
* deltaLpUsdc: number|null,
|
|
38
|
+
* targetHedgeUsdc: number|null,
|
|
39
|
+
* minTimeToExpirySec: number|null
|
|
40
|
+
* }}
|
|
41
|
+
*/
|
|
42
|
+
function evaluateSnapshot(verifyPayload, options) {
|
|
43
|
+
const pandora = verifyPayload.pandora || {};
|
|
44
|
+
const source = verifyPayload.sourceMarket || {};
|
|
45
|
+
|
|
46
|
+
const sourceYes = toNumber(source.yesPct);
|
|
47
|
+
const pandoraYes = toNumber(pandora.yesPct);
|
|
48
|
+
|
|
49
|
+
const driftBps = sourceYes !== null && pandoraYes !== null ? Math.abs(sourceYes - pandoraYes) * 100 : null;
|
|
50
|
+
const driftTriggered = driftBps !== null && driftBps >= options.driftTriggerBps;
|
|
51
|
+
|
|
52
|
+
const reserveYes = toNumber(pandora.reserveYes);
|
|
53
|
+
const reserveNo = toNumber(pandora.reserveNo);
|
|
54
|
+
const reserveTotalUsdc = reserveYes !== null && reserveNo !== null ? round(reserveYes + reserveNo, 6) : null;
|
|
55
|
+
const deltaLpUsdc = reserveYes !== null && reserveNo !== null ? round(reserveYes - reserveNo, 6) : null;
|
|
56
|
+
const targetHedgeUsdc = deltaLpUsdc === null ? null : round(-deltaLpUsdc, 6);
|
|
57
|
+
const minTimeToExpirySec = readMinTimeToExpirySec(verifyPayload);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
sourceYesPct: sourceYes,
|
|
61
|
+
pandoraYesPct: pandoraYes,
|
|
62
|
+
driftBps,
|
|
63
|
+
driftTriggered,
|
|
64
|
+
reserveYesUsdc: reserveYes,
|
|
65
|
+
reserveNoUsdc: reserveNo,
|
|
66
|
+
reserveTotalUsdc,
|
|
67
|
+
deltaLpUsdc,
|
|
68
|
+
targetHedgeUsdc,
|
|
69
|
+
minTimeToExpirySec,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Evaluate hard execution gates before submitting live sync actions.
|
|
75
|
+
* @param {object} context
|
|
76
|
+
* @returns {{ok: boolean, failedChecks: string[], checks: Array<{code: string, ok: boolean, message: string, details: any}>}}
|
|
77
|
+
*/
|
|
78
|
+
function evaluateStrictGates(context) {
|
|
79
|
+
const failures = [];
|
|
80
|
+
const checks = [];
|
|
81
|
+
|
|
82
|
+
const add = (code, ok, message, details = null) => {
|
|
83
|
+
checks.push({ code, ok, message, details });
|
|
84
|
+
if (!ok) failures.push(code);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const verify = context.verifyPayload;
|
|
88
|
+
const verifyGate = verify && verify.gateResult ? verify.gateResult : null;
|
|
89
|
+
add('MATCH_AND_RULES', Boolean(verifyGate && verifyGate.ok), 'Mirror match/rules gates must pass.', {
|
|
90
|
+
failedChecks: verifyGate ? verifyGate.failedChecks : ['UNKNOWN'],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const sourceType = String((verify && verify.sourceMarket && verify.sourceMarket.source) || '').toLowerCase();
|
|
94
|
+
const sourceIsCached = sourceType === 'polymarket:cache';
|
|
95
|
+
add(
|
|
96
|
+
'POLYMARKET_SOURCE_FRESH',
|
|
97
|
+
context.executeLive ? !sourceIsCached : true,
|
|
98
|
+
'Live mode requires fresh Polymarket source data (cached source is blocked).',
|
|
99
|
+
{
|
|
100
|
+
sourceType: sourceType || null,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const closeDeltaCheck =
|
|
105
|
+
verifyGate && Array.isArray(verifyGate.checks)
|
|
106
|
+
? verifyGate.checks.find((item) => item.code === 'CLOSE_TIME_DELTA')
|
|
107
|
+
: null;
|
|
108
|
+
add(
|
|
109
|
+
'CLOSE_TIME_DELTA',
|
|
110
|
+
closeDeltaCheck ? closeDeltaCheck.ok : true,
|
|
111
|
+
'Close-time delta must be within strict threshold.',
|
|
112
|
+
closeDeltaCheck && closeDeltaCheck.meta ? closeDeltaCheck.meta : null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const depthRequired = toNumber(context.plannedHedgeUsdc) || 0;
|
|
116
|
+
const explicitHedgeDepth = toNumber(context.hedgeDepthWithinSlippageUsd);
|
|
117
|
+
const depthAvailable = explicitHedgeDepth === null ? toNumber(context.depthWithinSlippageUsd) || 0 : explicitHedgeDepth;
|
|
118
|
+
add(
|
|
119
|
+
'DEPTH_COVERAGE',
|
|
120
|
+
depthRequired <= 0 ? true : depthAvailable >= depthRequired,
|
|
121
|
+
'Source depth must cover hedge notional at configured slippage.',
|
|
122
|
+
{
|
|
123
|
+
depthRequired,
|
|
124
|
+
depthAvailable,
|
|
125
|
+
slippageBps: context.depthSlippageBps,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const state = context.state;
|
|
130
|
+
const postTradeHedgeExposure = Math.abs(
|
|
131
|
+
(toNumber(state.currentHedgeUsdc) || 0) + (toNumber(context.plannedHedgeSignedUsdc) || 0),
|
|
132
|
+
);
|
|
133
|
+
const maxExposure = toNumber(context.maxOpenExposureUsdc);
|
|
134
|
+
add(
|
|
135
|
+
'MAX_OPEN_EXPOSURE',
|
|
136
|
+
maxExposure === null ? true : postTradeHedgeExposure <= maxExposure,
|
|
137
|
+
'Max open exposure must not be exceeded.',
|
|
138
|
+
{
|
|
139
|
+
postTradeHedgeExposure: round(postTradeHedgeExposure, 6),
|
|
140
|
+
maxOpenExposureUsdc: maxExposure,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
add(
|
|
145
|
+
'MAX_TRADES_PER_DAY',
|
|
146
|
+
(toNumber(state.tradesToday) || 0) < context.maxTradesPerDay,
|
|
147
|
+
'Daily trade cap must allow another execution.',
|
|
148
|
+
{
|
|
149
|
+
tradesToday: state.tradesToday,
|
|
150
|
+
maxTradesPerDay: context.maxTradesPerDay,
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
add(
|
|
155
|
+
'MIN_TIME_TO_EXPIRY',
|
|
156
|
+
context.minTimeToExpirySec === null ? true : context.minTimeToExpirySec >= context.minimumTimeToCloseSec,
|
|
157
|
+
`Minimum time-to-expiry must be >= ${context.minimumTimeToCloseSec}s for sync runtime.`,
|
|
158
|
+
{
|
|
159
|
+
minTimeToExpirySec: context.minTimeToExpirySec,
|
|
160
|
+
minimumTimeToCloseSec: context.minimumTimeToCloseSec,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
ok: failures.length === 0,
|
|
166
|
+
failedChecks: failures,
|
|
167
|
+
checks,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeSkipGateChecks(rawChecks) {
|
|
172
|
+
if (!Array.isArray(rawChecks)) return [];
|
|
173
|
+
const allowed = new Set(MIRROR_SYNC_GATE_CODES);
|
|
174
|
+
return Array.from(
|
|
175
|
+
new Set(
|
|
176
|
+
rawChecks
|
|
177
|
+
.map((value) => String(value || '').trim().toUpperCase())
|
|
178
|
+
.filter((value) => value && allowed.has(value)),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyGateBypassPolicy(gate, options) {
|
|
184
|
+
const failedChecksRaw = Array.isArray(gate && gate.failedChecks) ? [...gate.failedChecks] : [];
|
|
185
|
+
const forceGate = Boolean(options && options.forceGate);
|
|
186
|
+
const skipGateChecks = forceGate ? [] : normalizeSkipGateChecks(options && options.skipGateChecks);
|
|
187
|
+
const skipSet = new Set(skipGateChecks);
|
|
188
|
+
const bypassedFailedChecks = forceGate
|
|
189
|
+
? failedChecksRaw
|
|
190
|
+
: failedChecksRaw.filter((code) => skipSet.has(code));
|
|
191
|
+
const failedChecks = forceGate
|
|
192
|
+
? []
|
|
193
|
+
: failedChecksRaw.filter((code) => !skipSet.has(code));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...(gate || { checks: [] }),
|
|
197
|
+
ok: failedChecks.length === 0,
|
|
198
|
+
failedChecks,
|
|
199
|
+
failedChecksRaw,
|
|
200
|
+
bypassedFailedChecks,
|
|
201
|
+
skipGateChecksApplied: forceGate ? ['*'] : skipGateChecks,
|
|
202
|
+
forceGate,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve runtime minimum time-to-close guard in seconds.
|
|
208
|
+
* @param {object} options
|
|
209
|
+
* @returns {number}
|
|
210
|
+
*/
|
|
211
|
+
function resolveMinimumTimeToCloseSec(options) {
|
|
212
|
+
const configuredMinTimeToCloseSec = Number.isInteger(Number(options.minTimeToCloseSec))
|
|
213
|
+
? Math.max(60, Math.trunc(Number(options.minTimeToCloseSec)))
|
|
214
|
+
: 1800;
|
|
215
|
+
return Math.max(
|
|
216
|
+
Math.ceil((options.intervalMs || 5_000) / 1000) * 2,
|
|
217
|
+
configuredMinTimeToCloseSec,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build strict-gate evaluation context for a tick.
|
|
223
|
+
* @param {{verifyPayload: object, options: object, state: object, plan: object, snapshotMetrics: object, depth: object, minimumTimeToCloseSec: number}} params
|
|
224
|
+
* @returns {object}
|
|
225
|
+
*/
|
|
226
|
+
function buildTickGateContext(params) {
|
|
227
|
+
const { verifyPayload, options, state, plan, snapshotMetrics, depth, minimumTimeToCloseSec } = params;
|
|
228
|
+
return {
|
|
229
|
+
verifyPayload,
|
|
230
|
+
executeLive: options.executeLive,
|
|
231
|
+
state,
|
|
232
|
+
plannedHedgeUsdc: plan.plannedHedgeUsdc,
|
|
233
|
+
plannedSpendUsdc: plan.plannedSpendUsdc,
|
|
234
|
+
plannedHedgeSignedUsdc: plan.plannedHedgeSignedUsdc,
|
|
235
|
+
minTimeToExpirySec: snapshotMetrics.minTimeToExpirySec,
|
|
236
|
+
minimumTimeToCloseSec,
|
|
237
|
+
depthWithinSlippageUsd: depth.depthWithinSlippageUsd,
|
|
238
|
+
hedgeDepthWithinSlippageUsd:
|
|
239
|
+
plan.plannedHedgeUsdc > 0
|
|
240
|
+
? plan.gapUsdc >= 0
|
|
241
|
+
? depth.yesDepth && depth.yesDepth.depthUsd
|
|
242
|
+
: depth.noDepth && depth.noDepth.depthUsd
|
|
243
|
+
: null,
|
|
244
|
+
depthSlippageBps: options.depthSlippageBps,
|
|
245
|
+
maxOpenExposureUsdc: options.maxOpenExposureUsdc,
|
|
246
|
+
maxTradesPerDay: options.maxTradesPerDay,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Verify mirror pair once at startup and enforce expiry guard.
|
|
252
|
+
* @param {{verifyFn: Function, options: object, minimumTimeToCloseSec: number, buildVerifyRequest: Function, createServiceError: Function}} params
|
|
253
|
+
* @returns {Promise<object>}
|
|
254
|
+
*/
|
|
255
|
+
async function runStartupVerify(params) {
|
|
256
|
+
const { verifyFn, options, minimumTimeToCloseSec, buildVerifyRequest, createServiceError } = params;
|
|
257
|
+
const payload = await verifyFn(buildVerifyRequest(options));
|
|
258
|
+
const startupMinTime = readMinTimeToExpirySec(payload);
|
|
259
|
+
if (startupMinTime !== null && startupMinTime < minimumTimeToCloseSec) {
|
|
260
|
+
throw createServiceError(
|
|
261
|
+
'MIRROR_EXPIRY_TOO_CLOSE',
|
|
262
|
+
`Mirror sync refused to start because market expiry is too close (${startupMinTime}s < ${minimumTimeToCloseSec}s).`,
|
|
263
|
+
{
|
|
264
|
+
startupMinTimeToExpirySec: startupMinTime,
|
|
265
|
+
minimumTimeToCloseSec,
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return payload;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
MIRROR_SYNC_GATE_CODES,
|
|
274
|
+
readMinTimeToExpirySec,
|
|
275
|
+
evaluateSnapshot,
|
|
276
|
+
evaluateStrictGates,
|
|
277
|
+
normalizeSkipGateChecks,
|
|
278
|
+
applyGateBypassPolicy,
|
|
279
|
+
resolveMinimumTimeToCloseSec,
|
|
280
|
+
buildTickGateContext,
|
|
281
|
+
runStartupVerify,
|
|
282
|
+
};
|