pandora-cli-skills 1.1.14 → 1.1.16
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
CHANGED
|
@@ -91,7 +91,7 @@ Prerequisite: Node.js `>=18`.
|
|
|
91
91
|
- `pandora export --wallet <0x...> --format csv --year 2026 --out ./trades-2026.csv`
|
|
92
92
|
- `pandora arbitrage --venues pandora,polymarket --min-spread-pct 3`
|
|
93
93
|
- `pandora autopilot run|once ...`
|
|
94
|
-
- `pandora mirror browse|plan|deploy|verify|go|sync|status|close ...`
|
|
94
|
+
- `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close ...`
|
|
95
95
|
- `pandora webhook test ...`
|
|
96
96
|
- `pandora leaderboard --metric profit|volume|win-rate`
|
|
97
97
|
- `pandora analyze --market-address <0x...> --provider <name>`
|
|
@@ -118,6 +118,9 @@ Prerequisite: Node.js `>=18`.
|
|
|
118
118
|
- `pandora mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity`
|
|
119
119
|
- `pandora mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper`
|
|
120
120
|
- `pandora mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity`
|
|
121
|
+
- `pandora mirror lp-explain --liquidity-usdc 10000 --source-yes-pct 58`
|
|
122
|
+
- `pandora mirror hedge-calc --reserve-yes-usdc 8 --reserve-no-usdc 12 --excess-no-usdc 2 --polymarket-yes-pct 60`
|
|
123
|
+
- `pandora mirror simulate --liquidity-usdc 10000 --source-yes-pct 58 --target-yes-pct 58 --volume-scenarios 1000,5000,10000`
|
|
121
124
|
- `pandora mirror sync once --pandora-market-address <0x...> --polymarket-market-id <id> --paper --hedge-ratio 1.0`
|
|
122
125
|
- `pandora mirror status --strategy-hash <hash> --with-live`
|
|
123
126
|
- `pandora mirror close --pandora-market-address <0x...> --polymarket-market-id <id> --dry-run`
|
|
@@ -198,6 +201,14 @@ Prerequisite: Node.js `>=18`.
|
|
|
198
201
|
- envelope is `ok=true`, `command="mirror.deploy"`, with `data.planDigest`, `data.deploymentArgs`, `data.tx`, `data.preflight`, `data.pandora`, `data.postDeployChecks`, and optional `data.trustManifest`.
|
|
199
202
|
- `mirror verify`:
|
|
200
203
|
- envelope is `ok=true`, `command="mirror.verify"`, with `data.matchConfidence`, `data.ruleHashLeft`, `data.ruleHashRight`, `data.ruleDiffSummary`, `data.expiry`, optional `data.trustManifest`, `data.similarityChecks[]`, and `data.gateResult`.
|
|
204
|
+
- `mirror lp-explain`:
|
|
205
|
+
- envelope is `ok=true`, `command="mirror.lp-explain"`, with complete-set flow fields (`mintedCompleteSets`, `seededPoolReserves`, `returnedExcessTokens`, `totalLpInventory`).
|
|
206
|
+
- this command explains the `addLiquidity` mechanics explicitly: complete sets are minted first, only a weighted slice is seeded into pool reserves, and excess YES/NO tokens are returned to LP wallet inventory.
|
|
207
|
+
- `mirror hedge-calc`:
|
|
208
|
+
- envelope is `ok=true`, `command="mirror.hedge-calc"`, with `data.metrics` (`deltaPoolUsdc`, `deltaTotalUsdc`, `targetHedgeUsdcSigned`, `hedgeToken`, `hedgeSharesApprox`, `breakEvenVolumeUsdc`) and `data.scenarios[]` fee-vs-hedge estimates.
|
|
209
|
+
- `mirror simulate`:
|
|
210
|
+
- envelope is `ok=true`, `command="mirror.simulate"`, with `data.initialState`, `data.targeting`, and `data.scenarios[]` for volume-based LP/hedge planning.
|
|
211
|
+
- simulation keeps the complete-set split exact (raw integer math), then models directional AMM flow with fee-in-reserve behavior for planning-grade projections.
|
|
201
212
|
- `mirror go`:
|
|
202
213
|
- envelope is `ok=true`, `command="mirror.go"`, with staged results for `data.plan`, `data.deploy`, `data.verify`, optional `data.sync`, and `data.suggestedSyncCommand`.
|
|
203
214
|
- `mirror sync`:
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
const { computeDistributionHint, normalizeProbability } = require('./mirror_sizing_service.cjs');
|
|
2
|
+
|
|
3
|
+
const MIRROR_LP_EXPLAIN_SCHEMA_VERSION = '1.0.0';
|
|
4
|
+
const MIRROR_HEDGE_CALC_SCHEMA_VERSION = '1.0.0';
|
|
5
|
+
const MIRROR_SIMULATE_SCHEMA_VERSION = '1.0.0';
|
|
6
|
+
const USDC_DECIMALS = 6;
|
|
7
|
+
const USDC_SCALE = 10 ** USDC_DECIMALS;
|
|
8
|
+
|
|
9
|
+
function toNumber(value) {
|
|
10
|
+
if (value === null || value === undefined || value === '') return null;
|
|
11
|
+
const numeric = Number(value);
|
|
12
|
+
if (!Number.isFinite(numeric)) return null;
|
|
13
|
+
return numeric;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function round(value, decimals = 6) {
|
|
17
|
+
if (!Number.isFinite(value)) return null;
|
|
18
|
+
const factor = 10 ** decimals;
|
|
19
|
+
return Math.round(value * factor) / factor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clamp(value, min, max) {
|
|
23
|
+
return Math.max(min, Math.min(max, value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toRawUsdc(usdc) {
|
|
27
|
+
const numeric = toNumber(usdc);
|
|
28
|
+
if (numeric === null) return null;
|
|
29
|
+
return BigInt(Math.round(numeric * USDC_SCALE));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rawUsdcToNumber(rawValue) {
|
|
33
|
+
if (typeof rawValue !== 'bigint') return null;
|
|
34
|
+
return round(Number(rawValue) / USDC_SCALE, 6);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toPercent(value) {
|
|
38
|
+
const numeric = toNumber(value);
|
|
39
|
+
if (numeric === null) return null;
|
|
40
|
+
if (numeric >= 0 && numeric <= 1) return round(numeric * 100, 6);
|
|
41
|
+
if (numeric >= 0 && numeric <= 100) return round(numeric, 6);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function yesPctToProbability(yesPct) {
|
|
46
|
+
const normalized = normalizeProbability(yesPct);
|
|
47
|
+
if (normalized === null) return null;
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveDistribution(options = {}) {
|
|
52
|
+
const diagnostics = [];
|
|
53
|
+
const sourceYesPct = toPercent(options.sourceYesPct);
|
|
54
|
+
|
|
55
|
+
const explicitYes = toNumber(options.distributionYes);
|
|
56
|
+
const explicitNo = toNumber(options.distributionNo);
|
|
57
|
+
if (explicitYes !== null || explicitNo !== null) {
|
|
58
|
+
if (!Number.isInteger(explicitYes) || !Number.isInteger(explicitNo)) {
|
|
59
|
+
throw new Error('distributionYes and distributionNo must be integers.');
|
|
60
|
+
}
|
|
61
|
+
if (explicitYes < 0 || explicitNo < 0 || explicitYes + explicitNo !== 1_000_000_000) {
|
|
62
|
+
throw new Error('distributionYes + distributionNo must equal 1000000000.');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
sourceYesPct,
|
|
66
|
+
sourceYesProbability: sourceYesPct === null ? null : round(sourceYesPct / 100, 6),
|
|
67
|
+
distributionYes: explicitYes,
|
|
68
|
+
distributionNo: explicitNo,
|
|
69
|
+
diagnostics,
|
|
70
|
+
mode: 'explicit',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const distribution = computeDistributionHint(sourceYesPct === null ? 50 : sourceYesPct);
|
|
75
|
+
if (sourceYesPct === null) {
|
|
76
|
+
diagnostics.push('No source YES probability supplied; defaulted to balanced 50/50 distribution hint.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sourceYesPct,
|
|
81
|
+
sourceYesProbability:
|
|
82
|
+
sourceYesPct === null
|
|
83
|
+
? null
|
|
84
|
+
: round(sourceYesPct / 100, 6),
|
|
85
|
+
distributionYes: distribution.distributionYes,
|
|
86
|
+
distributionNo: distribution.distributionNo,
|
|
87
|
+
diagnostics: diagnostics.concat(distribution.diagnostics || []),
|
|
88
|
+
mode: sourceYesPct === null ? 'default' : 'derived-from-source-yes-pct',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function deriveYesPctFromReserves(reserveYesUsdc, reserveNoUsdc) {
|
|
93
|
+
const yesReserve = toNumber(reserveYesUsdc);
|
|
94
|
+
const noReserve = toNumber(reserveNoUsdc);
|
|
95
|
+
if (yesReserve === null || noReserve === null) return null;
|
|
96
|
+
const total = yesReserve + noReserve;
|
|
97
|
+
if (!Number.isFinite(total) || total <= 0) return null;
|
|
98
|
+
return round((noReserve / total) * 100, 6);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function computeCompleteSetAllocation(options = {}) {
|
|
102
|
+
const liquidityRaw = toRawUsdc(options.liquidityUsdc);
|
|
103
|
+
if (liquidityRaw === null || liquidityRaw <= 0n) {
|
|
104
|
+
throw new Error('liquidityUsdc must be a positive number.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const distributionYes = toNumber(options.distributionYes);
|
|
108
|
+
const distributionNo = toNumber(options.distributionNo);
|
|
109
|
+
if (!Number.isInteger(distributionYes) || !Number.isInteger(distributionNo)) {
|
|
110
|
+
throw new Error('distributionYes and distributionNo must be integers.');
|
|
111
|
+
}
|
|
112
|
+
if (distributionYes < 0 || distributionNo < 0 || distributionYes + distributionNo !== 1_000_000_000) {
|
|
113
|
+
throw new Error('distributionYes + distributionNo must equal 1000000000.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const yesWeight = BigInt(distributionYes);
|
|
117
|
+
const noWeight = BigInt(distributionNo);
|
|
118
|
+
|
|
119
|
+
const mintedYesRaw = liquidityRaw;
|
|
120
|
+
const mintedNoRaw = liquidityRaw;
|
|
121
|
+
|
|
122
|
+
let reserveYesRaw;
|
|
123
|
+
let reserveNoRaw;
|
|
124
|
+
if (noWeight >= yesWeight) {
|
|
125
|
+
reserveYesRaw = liquidityRaw;
|
|
126
|
+
reserveNoRaw = noWeight === 0n ? 0n : (liquidityRaw * yesWeight) / noWeight;
|
|
127
|
+
} else {
|
|
128
|
+
reserveNoRaw = liquidityRaw;
|
|
129
|
+
reserveYesRaw = yesWeight === 0n ? 0n : (liquidityRaw * noWeight) / yesWeight;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const excessYesRaw = mintedYesRaw - reserveYesRaw;
|
|
133
|
+
const excessNoRaw = mintedNoRaw - reserveNoRaw;
|
|
134
|
+
const totalYesRaw = reserveYesRaw + excessYesRaw;
|
|
135
|
+
const totalNoRaw = reserveNoRaw + excessNoRaw;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
liquidityRaw,
|
|
139
|
+
mintedYesRaw,
|
|
140
|
+
mintedNoRaw,
|
|
141
|
+
reserveYesRaw,
|
|
142
|
+
reserveNoRaw,
|
|
143
|
+
excessYesRaw,
|
|
144
|
+
excessNoRaw,
|
|
145
|
+
totalYesRaw,
|
|
146
|
+
totalNoRaw,
|
|
147
|
+
impliedPoolYesPct: deriveYesPctFromReserves(rawUsdcToNumber(reserveYesRaw), rawUsdcToNumber(reserveNoRaw)),
|
|
148
|
+
liquidityUsdc: rawUsdcToNumber(liquidityRaw),
|
|
149
|
+
mintedYesUsdc: rawUsdcToNumber(mintedYesRaw),
|
|
150
|
+
mintedNoUsdc: rawUsdcToNumber(mintedNoRaw),
|
|
151
|
+
reserveYesUsdc: rawUsdcToNumber(reserveYesRaw),
|
|
152
|
+
reserveNoUsdc: rawUsdcToNumber(reserveNoRaw),
|
|
153
|
+
excessYesUsdc: rawUsdcToNumber(excessYesRaw),
|
|
154
|
+
excessNoUsdc: rawUsdcToNumber(excessNoRaw),
|
|
155
|
+
totalYesUsdc: rawUsdcToNumber(totalYesRaw),
|
|
156
|
+
totalNoUsdc: rawUsdcToNumber(totalNoRaw),
|
|
157
|
+
neutralCompleteSets: totalYesRaw === totalNoRaw,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function simulateDirectionalSwap(options = {}) {
|
|
162
|
+
const reserveYes = Math.max(0, toNumber(options.reserveYesUsdc) || 0);
|
|
163
|
+
const reserveNo = Math.max(0, toNumber(options.reserveNoUsdc) || 0);
|
|
164
|
+
const volumeUsdc = Math.max(0, toNumber(options.volumeUsdc) || 0);
|
|
165
|
+
const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
|
|
166
|
+
const feeRate = clamp(feeTier / 1_000_000, 0, 0.1);
|
|
167
|
+
const side = String(options.side || 'none').toLowerCase();
|
|
168
|
+
|
|
169
|
+
if (side !== 'yes' && side !== 'no') {
|
|
170
|
+
return {
|
|
171
|
+
side: 'none',
|
|
172
|
+
volumeUsdc: round(volumeUsdc, 6),
|
|
173
|
+
feeTier,
|
|
174
|
+
feeRate: round(feeRate, 8),
|
|
175
|
+
feesEarnedUsdc: 0,
|
|
176
|
+
outputShares: 0,
|
|
177
|
+
reserveYesUsdc: round(reserveYes, 6),
|
|
178
|
+
reserveNoUsdc: round(reserveNo, 6),
|
|
179
|
+
postYesPct: deriveYesPctFromReserves(reserveYes, reserveNo),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const effectiveIn = volumeUsdc * (1 - feeRate);
|
|
184
|
+
const feesEarnedUsdc = volumeUsdc - effectiveIn;
|
|
185
|
+
|
|
186
|
+
if (side === 'yes') {
|
|
187
|
+
const denominator = reserveNo + effectiveIn;
|
|
188
|
+
const outputYes = denominator > 0 ? (reserveYes * effectiveIn) / denominator : 0;
|
|
189
|
+
const postReserveYes = Math.max(0, reserveYes - outputYes);
|
|
190
|
+
const postReserveNo = reserveNo + volumeUsdc;
|
|
191
|
+
return {
|
|
192
|
+
side,
|
|
193
|
+
volumeUsdc: round(volumeUsdc, 6),
|
|
194
|
+
feeTier,
|
|
195
|
+
feeRate: round(feeRate, 8),
|
|
196
|
+
feesEarnedUsdc: round(feesEarnedUsdc, 6),
|
|
197
|
+
outputShares: round(outputYes, 6),
|
|
198
|
+
reserveYesUsdc: round(postReserveYes, 6),
|
|
199
|
+
reserveNoUsdc: round(postReserveNo, 6),
|
|
200
|
+
postYesPct: deriveYesPctFromReserves(postReserveYes, postReserveNo),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const denominator = reserveYes + effectiveIn;
|
|
205
|
+
const outputNo = denominator > 0 ? (reserveNo * effectiveIn) / denominator : 0;
|
|
206
|
+
const postReserveNo = Math.max(0, reserveNo - outputNo);
|
|
207
|
+
const postReserveYes = reserveYes + volumeUsdc;
|
|
208
|
+
return {
|
|
209
|
+
side,
|
|
210
|
+
volumeUsdc: round(volumeUsdc, 6),
|
|
211
|
+
feeTier,
|
|
212
|
+
feeRate: round(feeRate, 8),
|
|
213
|
+
feesEarnedUsdc: round(feesEarnedUsdc, 6),
|
|
214
|
+
outputShares: round(outputNo, 6),
|
|
215
|
+
reserveYesUsdc: round(postReserveYes, 6),
|
|
216
|
+
reserveNoUsdc: round(postReserveNo, 6),
|
|
217
|
+
postYesPct: deriveYesPctFromReserves(postReserveYes, postReserveNo),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function solveVolumeForTargetYesPct(options = {}) {
|
|
222
|
+
const targetYesPct = toPercent(options.targetYesPct);
|
|
223
|
+
const reserveYesUsdc = toNumber(options.reserveYesUsdc);
|
|
224
|
+
const reserveNoUsdc = toNumber(options.reserveNoUsdc);
|
|
225
|
+
const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
|
|
226
|
+
|
|
227
|
+
if (targetYesPct === null || reserveYesUsdc === null || reserveNoUsdc === null) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const initialYesPct = deriveYesPctFromReserves(reserveYesUsdc, reserveNoUsdc);
|
|
232
|
+
if (initialYesPct === null) return null;
|
|
233
|
+
if (Math.abs(initialYesPct - targetYesPct) <= 0.01) return 0;
|
|
234
|
+
|
|
235
|
+
const side = targetYesPct > initialYesPct ? 'yes' : 'no';
|
|
236
|
+
const directionCheck = (yesPct) => (side === 'yes' ? yesPct >= targetYesPct : yesPct <= targetYesPct);
|
|
237
|
+
const f = (volumeUsdc) =>
|
|
238
|
+
simulateDirectionalSwap({
|
|
239
|
+
reserveYesUsdc,
|
|
240
|
+
reserveNoUsdc,
|
|
241
|
+
side,
|
|
242
|
+
volumeUsdc,
|
|
243
|
+
feeTier,
|
|
244
|
+
}).postYesPct;
|
|
245
|
+
|
|
246
|
+
let lo = 0;
|
|
247
|
+
let hi = Math.max(1, reserveYesUsdc + reserveNoUsdc);
|
|
248
|
+
let yesAtHi = f(hi);
|
|
249
|
+
let iterations = 0;
|
|
250
|
+
while (!directionCheck(yesAtHi) && iterations < 32) {
|
|
251
|
+
hi *= 2;
|
|
252
|
+
yesAtHi = f(hi);
|
|
253
|
+
iterations += 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!directionCheck(yesAtHi)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < 56; i += 1) {
|
|
261
|
+
const mid = (lo + hi) / 2;
|
|
262
|
+
const yesAtMid = f(mid);
|
|
263
|
+
if (directionCheck(yesAtMid)) {
|
|
264
|
+
hi = mid;
|
|
265
|
+
} else {
|
|
266
|
+
lo = mid;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return round(hi, 6);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeVolumeScenarios(value, liquidityUsdc) {
|
|
274
|
+
if (Array.isArray(value)) {
|
|
275
|
+
return value.map((item) => toNumber(item)).filter((item) => Number.isFinite(item) && item >= 0).map((item) => round(item, 6));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const raw = String(value || '').trim();
|
|
279
|
+
if (raw) {
|
|
280
|
+
return raw
|
|
281
|
+
.split(',')
|
|
282
|
+
.map((entry) => toNumber(entry.trim()))
|
|
283
|
+
.filter((entry) => Number.isFinite(entry) && entry >= 0)
|
|
284
|
+
.map((entry) => round(entry, 6));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const liq = Math.max(1, toNumber(liquidityUsdc) || 0);
|
|
288
|
+
return [
|
|
289
|
+
round(liq * 0.25, 6),
|
|
290
|
+
round(liq * 0.5, 6),
|
|
291
|
+
round(liq, 6),
|
|
292
|
+
round(liq * 2, 6),
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function computeHedgeMetrics(options = {}) {
|
|
297
|
+
const reserveYesUsdc = toNumber(options.reserveYesUsdc) || 0;
|
|
298
|
+
const reserveNoUsdc = toNumber(options.reserveNoUsdc) || 0;
|
|
299
|
+
const excessYesUsdc = toNumber(options.excessYesUsdc) || 0;
|
|
300
|
+
const excessNoUsdc = toNumber(options.excessNoUsdc) || 0;
|
|
301
|
+
const hedgeRatio = clamp(toNumber(options.hedgeRatio) || 1, 0, 5);
|
|
302
|
+
const polymarketYesPct = toPercent(options.polymarketYesPct);
|
|
303
|
+
const hedgeCostBps = Math.max(0, toNumber(options.hedgeCostBps) || 35);
|
|
304
|
+
const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
|
|
305
|
+
const feeRate = clamp(feeTier / 1_000_000, 0, 0.1);
|
|
306
|
+
|
|
307
|
+
const deltaPoolUsdc = reserveYesUsdc - reserveNoUsdc;
|
|
308
|
+
const deltaTotalUsdc = (reserveYesUsdc + excessYesUsdc) - (reserveNoUsdc + excessNoUsdc);
|
|
309
|
+
const targetHedgeUsdcSigned = -deltaTotalUsdc * hedgeRatio;
|
|
310
|
+
const targetHedgeUsdcAbs = Math.abs(targetHedgeUsdcSigned);
|
|
311
|
+
const hedgeToken = targetHedgeUsdcSigned > 0 ? 'yes' : targetHedgeUsdcSigned < 0 ? 'no' : null;
|
|
312
|
+
|
|
313
|
+
const yesPrice01 = polymarketYesPct === null ? null : clamp(polymarketYesPct / 100, 0.0001, 0.9999);
|
|
314
|
+
const noPrice01 = yesPrice01 === null ? null : 1 - yesPrice01;
|
|
315
|
+
const hedgePrice01 = hedgeToken === 'yes' ? yesPrice01 : hedgeToken === 'no' ? noPrice01 : null;
|
|
316
|
+
const hedgeSharesApprox = hedgePrice01 ? round(targetHedgeUsdcAbs / hedgePrice01, 6) : null;
|
|
317
|
+
|
|
318
|
+
const hedgeCostApproxUsdc = round(targetHedgeUsdcAbs * (hedgeCostBps / 10_000), 6) || 0;
|
|
319
|
+
const breakEvenVolumeUsdc = feeRate > 0 ? round(hedgeCostApproxUsdc / feeRate, 6) : null;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
reserveYesUsdc: round(reserveYesUsdc, 6),
|
|
323
|
+
reserveNoUsdc: round(reserveNoUsdc, 6),
|
|
324
|
+
excessYesUsdc: round(excessYesUsdc, 6),
|
|
325
|
+
excessNoUsdc: round(excessNoUsdc, 6),
|
|
326
|
+
deltaPoolUsdc: round(deltaPoolUsdc, 6),
|
|
327
|
+
deltaTotalUsdc: round(deltaTotalUsdc, 6),
|
|
328
|
+
hedgeRatio: round(hedgeRatio, 6),
|
|
329
|
+
targetHedgeUsdcSigned: round(targetHedgeUsdcSigned, 6),
|
|
330
|
+
targetHedgeUsdcAbs: round(targetHedgeUsdcAbs, 6),
|
|
331
|
+
hedgeToken,
|
|
332
|
+
polymarketYesPct,
|
|
333
|
+
polymarketNoPct: polymarketYesPct === null ? null : round(100 - polymarketYesPct, 6),
|
|
334
|
+
hedgePrice01: hedgePrice01 === null ? null : round(hedgePrice01, 8),
|
|
335
|
+
hedgeSharesApprox,
|
|
336
|
+
hedgeCostBps: round(hedgeCostBps, 6),
|
|
337
|
+
hedgeCostApproxUsdc,
|
|
338
|
+
feeTier,
|
|
339
|
+
feeRate: round(feeRate, 8),
|
|
340
|
+
breakEvenVolumeUsdc,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function buildMirrorLpExplain(options = {}) {
|
|
345
|
+
const distribution = resolveDistribution(options);
|
|
346
|
+
const allocation = computeCompleteSetAllocation({
|
|
347
|
+
liquidityUsdc: options.liquidityUsdc,
|
|
348
|
+
distributionYes: distribution.distributionYes,
|
|
349
|
+
distributionNo: distribution.distributionNo,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const diagnostics = [];
|
|
353
|
+
diagnostics.push(...distribution.diagnostics);
|
|
354
|
+
diagnostics.push(
|
|
355
|
+
'addLiquidity mints complete sets first, seeds reserves by distribution hint, then returns excess YES/NO tokens to the LP wallet.',
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
schemaVersion: MIRROR_LP_EXPLAIN_SCHEMA_VERSION,
|
|
360
|
+
generatedAt: new Date().toISOString(),
|
|
361
|
+
inputs: {
|
|
362
|
+
liquidityUsdc: allocation.liquidityUsdc,
|
|
363
|
+
sourceYesPct: distribution.sourceYesPct,
|
|
364
|
+
distributionYes: distribution.distributionYes,
|
|
365
|
+
distributionNo: distribution.distributionNo,
|
|
366
|
+
distributionMode: distribution.mode,
|
|
367
|
+
},
|
|
368
|
+
flow: {
|
|
369
|
+
mintedCompleteSets: {
|
|
370
|
+
yesTokens: allocation.mintedYesUsdc,
|
|
371
|
+
noTokens: allocation.mintedNoUsdc,
|
|
372
|
+
},
|
|
373
|
+
seededPoolReserves: {
|
|
374
|
+
reserveYesUsdc: allocation.reserveYesUsdc,
|
|
375
|
+
reserveNoUsdc: allocation.reserveNoUsdc,
|
|
376
|
+
impliedPandoraYesPct: allocation.impliedPoolYesPct,
|
|
377
|
+
},
|
|
378
|
+
returnedExcessTokens: {
|
|
379
|
+
excessYesUsdc: allocation.excessYesUsdc,
|
|
380
|
+
excessNoUsdc: allocation.excessNoUsdc,
|
|
381
|
+
},
|
|
382
|
+
totalLpInventory: {
|
|
383
|
+
totalYesUsdc: allocation.totalYesUsdc,
|
|
384
|
+
totalNoUsdc: allocation.totalNoUsdc,
|
|
385
|
+
deltaUsdc: round((allocation.totalYesUsdc || 0) - (allocation.totalNoUsdc || 0), 6),
|
|
386
|
+
neutralCompleteSets: allocation.neutralCompleteSets,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
raw: {
|
|
390
|
+
liquidityRaw: allocation.liquidityRaw.toString(),
|
|
391
|
+
reserveYesRaw: allocation.reserveYesRaw.toString(),
|
|
392
|
+
reserveNoRaw: allocation.reserveNoRaw.toString(),
|
|
393
|
+
excessYesRaw: allocation.excessYesRaw.toString(),
|
|
394
|
+
excessNoRaw: allocation.excessNoRaw.toString(),
|
|
395
|
+
},
|
|
396
|
+
diagnostics,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildMirrorHedgeCalc(options = {}) {
|
|
401
|
+
const scenarios = normalizeVolumeScenarios(options.volumeScenarios, options.reserveYesUsdc + options.reserveNoUsdc);
|
|
402
|
+
const metrics = computeHedgeMetrics(options);
|
|
403
|
+
|
|
404
|
+
const scenarioRows = scenarios.map((volumeUsdc) => {
|
|
405
|
+
const fees = round(volumeUsdc * (metrics.feeRate || 0), 6) || 0;
|
|
406
|
+
return {
|
|
407
|
+
volumeUsdc,
|
|
408
|
+
feeRevenueUsdc: fees,
|
|
409
|
+
hedgeCostApproxUsdc: metrics.hedgeCostApproxUsdc,
|
|
410
|
+
netPnlApproxUsdc: round(fees - (metrics.hedgeCostApproxUsdc || 0), 6),
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
schemaVersion: MIRROR_HEDGE_CALC_SCHEMA_VERSION,
|
|
416
|
+
generatedAt: new Date().toISOString(),
|
|
417
|
+
metrics,
|
|
418
|
+
scenarios: scenarioRows,
|
|
419
|
+
diagnostics: [
|
|
420
|
+
'Hedge sizing is notional-based and assumes FAK fills near the provided Polymarket mark price.',
|
|
421
|
+
'P&L rows are approximation: LP fee accrual minus estimated hedge execution cost.',
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function buildMirrorSimulate(options = {}) {
|
|
427
|
+
const distribution = resolveDistribution(options);
|
|
428
|
+
const allocation = computeCompleteSetAllocation({
|
|
429
|
+
liquidityUsdc: options.liquidityUsdc,
|
|
430
|
+
distributionYes: distribution.distributionYes,
|
|
431
|
+
distributionNo: distribution.distributionNo,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const targetYesPct = toPercent(options.targetYesPct !== undefined ? options.targetYesPct : distribution.sourceYesPct);
|
|
435
|
+
const initialYesPct = allocation.impliedPoolYesPct;
|
|
436
|
+
const tradeSide =
|
|
437
|
+
targetYesPct === null || initialYesPct === null
|
|
438
|
+
? 'none'
|
|
439
|
+
: targetYesPct > initialYesPct
|
|
440
|
+
? 'yes'
|
|
441
|
+
: targetYesPct < initialYesPct
|
|
442
|
+
? 'no'
|
|
443
|
+
: 'none';
|
|
444
|
+
|
|
445
|
+
const feeTier = Number.isFinite(Number(options.feeTier)) ? Number(options.feeTier) : 3000;
|
|
446
|
+
const hedgeRatio = clamp(toNumber(options.hedgeRatio) || 1, 0, 5);
|
|
447
|
+
const hedgeCostBps = Math.max(0, toNumber(options.hedgeCostBps) || 35);
|
|
448
|
+
const polymarketYesPct = toPercent(
|
|
449
|
+
options.polymarketYesPct !== undefined ? options.polymarketYesPct : (targetYesPct !== null ? targetYesPct : 50),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const volumeScenarios = normalizeVolumeScenarios(options.volumeScenarios, allocation.liquidityUsdc);
|
|
453
|
+
const volumeNeededToTargetUsdc =
|
|
454
|
+
targetYesPct === null
|
|
455
|
+
? null
|
|
456
|
+
: solveVolumeForTargetYesPct({
|
|
457
|
+
targetYesPct,
|
|
458
|
+
reserveYesUsdc: allocation.reserveYesUsdc,
|
|
459
|
+
reserveNoUsdc: allocation.reserveNoUsdc,
|
|
460
|
+
feeTier,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const scenarioResults = volumeScenarios.map((volumeUsdc) => {
|
|
464
|
+
const swap = simulateDirectionalSwap({
|
|
465
|
+
reserveYesUsdc: allocation.reserveYesUsdc,
|
|
466
|
+
reserveNoUsdc: allocation.reserveNoUsdc,
|
|
467
|
+
side: tradeSide,
|
|
468
|
+
volumeUsdc,
|
|
469
|
+
feeTier,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const hedge = computeHedgeMetrics({
|
|
473
|
+
reserveYesUsdc: swap.reserveYesUsdc,
|
|
474
|
+
reserveNoUsdc: swap.reserveNoUsdc,
|
|
475
|
+
excessYesUsdc: allocation.excessYesUsdc,
|
|
476
|
+
excessNoUsdc: allocation.excessNoUsdc,
|
|
477
|
+
hedgeRatio,
|
|
478
|
+
polymarketYesPct,
|
|
479
|
+
hedgeCostBps,
|
|
480
|
+
feeTier,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const netPnlApproxUsdc = round((swap.feesEarnedUsdc || 0) - (hedge.hedgeCostApproxUsdc || 0), 6);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
volumeUsdc: round(volumeUsdc, 6),
|
|
487
|
+
tradeSide,
|
|
488
|
+
feesEarnedUsdc: swap.feesEarnedUsdc,
|
|
489
|
+
postReserveYesUsdc: swap.reserveYesUsdc,
|
|
490
|
+
postReserveNoUsdc: swap.reserveNoUsdc,
|
|
491
|
+
postYesPct: swap.postYesPct,
|
|
492
|
+
driftToTargetBps:
|
|
493
|
+
targetYesPct === null || swap.postYesPct === null
|
|
494
|
+
? null
|
|
495
|
+
: round(Math.abs(targetYesPct - swap.postYesPct) * 100, 6),
|
|
496
|
+
hedge: {
|
|
497
|
+
targetHedgeUsdc: hedge.targetHedgeUsdcSigned,
|
|
498
|
+
hedgeToken: hedge.hedgeToken,
|
|
499
|
+
hedgeSharesApprox: hedge.hedgeSharesApprox,
|
|
500
|
+
hedgeCostApproxUsdc: hedge.hedgeCostApproxUsdc,
|
|
501
|
+
},
|
|
502
|
+
netPnlApproxUsdc,
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
schemaVersion: MIRROR_SIMULATE_SCHEMA_VERSION,
|
|
508
|
+
generatedAt: new Date().toISOString(),
|
|
509
|
+
inputs: {
|
|
510
|
+
liquidityUsdc: allocation.liquidityUsdc,
|
|
511
|
+
feeTier,
|
|
512
|
+
hedgeRatio,
|
|
513
|
+
hedgeCostBps,
|
|
514
|
+
sourceYesPct: distribution.sourceYesPct,
|
|
515
|
+
targetYesPct,
|
|
516
|
+
polymarketYesPct,
|
|
517
|
+
distributionYes: distribution.distributionYes,
|
|
518
|
+
distributionNo: distribution.distributionNo,
|
|
519
|
+
distributionMode: distribution.mode,
|
|
520
|
+
tradeSide,
|
|
521
|
+
volumeScenarios,
|
|
522
|
+
},
|
|
523
|
+
initialState: {
|
|
524
|
+
reserveYesUsdc: allocation.reserveYesUsdc,
|
|
525
|
+
reserveNoUsdc: allocation.reserveNoUsdc,
|
|
526
|
+
excessYesUsdc: allocation.excessYesUsdc,
|
|
527
|
+
excessNoUsdc: allocation.excessNoUsdc,
|
|
528
|
+
initialYesPct,
|
|
529
|
+
neutralCompleteSets: allocation.neutralCompleteSets,
|
|
530
|
+
},
|
|
531
|
+
targeting: {
|
|
532
|
+
volumeNeededToTargetUsdc,
|
|
533
|
+
targetReachable: volumeNeededToTargetUsdc !== null,
|
|
534
|
+
},
|
|
535
|
+
scenarios: scenarioResults,
|
|
536
|
+
diagnostics: [
|
|
537
|
+
'Complete-set mint/split step is exact (raw integer math).',
|
|
538
|
+
'Trade path models a CPMM-style directional flow with fees retained in reserves.',
|
|
539
|
+
'Use mirror sync + live orderbooks for execution-grade sizing; this command is planning-grade simulation.',
|
|
540
|
+
].concat(distribution.diagnostics || []),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
module.exports = {
|
|
545
|
+
MIRROR_LP_EXPLAIN_SCHEMA_VERSION,
|
|
546
|
+
MIRROR_HEDGE_CALC_SCHEMA_VERSION,
|
|
547
|
+
MIRROR_SIMULATE_SCHEMA_VERSION,
|
|
548
|
+
resolveDistribution,
|
|
549
|
+
computeCompleteSetAllocation,
|
|
550
|
+
simulateDirectionalSwap,
|
|
551
|
+
solveVolumeForTargetYesPct,
|
|
552
|
+
computeHedgeMetrics,
|
|
553
|
+
buildMirrorLpExplain,
|
|
554
|
+
buildMirrorHedgeCalc,
|
|
555
|
+
buildMirrorSimulate,
|
|
556
|
+
};
|
|
@@ -3,8 +3,10 @@ const { createIndexerClient } = require('./indexer_client.cjs');
|
|
|
3
3
|
const { resolvePolymarketMarket } = require('./polymarket_trade_adapter.cjs');
|
|
4
4
|
|
|
5
5
|
const MIRROR_VERIFY_SCHEMA_VERSION = '1.0.0';
|
|
6
|
+
const USDC_DECIMALS = 6;
|
|
6
7
|
|
|
7
8
|
function toNumber(value) {
|
|
9
|
+
if (value === null || value === undefined || value === '') return null;
|
|
8
10
|
const numeric = Number(value);
|
|
9
11
|
if (!Number.isFinite(numeric)) return null;
|
|
10
12
|
return numeric;
|
|
@@ -16,6 +18,12 @@ function round(value, decimals = 6) {
|
|
|
16
18
|
return Math.round(value * factor) / factor;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function normalizeUsdcRawToUsd(value) {
|
|
22
|
+
const numeric = toNumber(value);
|
|
23
|
+
if (numeric === null) return null;
|
|
24
|
+
return round(numeric / (10 ** USDC_DECIMALS), 6);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
function normalizeQuestion(question) {
|
|
20
28
|
return String(question || '')
|
|
21
29
|
.toLowerCase()
|
|
@@ -125,8 +133,8 @@ function derivePandoraYesPct(market) {
|
|
|
125
133
|
return round(yesFromChance * 100, 6);
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
const reserveYes =
|
|
129
|
-
const reserveNo =
|
|
136
|
+
const reserveYes = normalizeUsdcRawToUsd(market && market.reserveYes);
|
|
137
|
+
const reserveNo = normalizeUsdcRawToUsd(market && market.reserveNo);
|
|
130
138
|
if (reserveYes === null || reserveNo === null) return null;
|
|
131
139
|
|
|
132
140
|
const total = reserveYes + reserveNo;
|
|
@@ -251,8 +259,8 @@ async function fetchPandoraMarketContext(options = {}) {
|
|
|
251
259
|
closeTimestamp: toNumber(market.marketCloseTimestamp) || toNumber(poll && poll.deadlineEpoch),
|
|
252
260
|
yesPct,
|
|
253
261
|
noPct,
|
|
254
|
-
reserveYes:
|
|
255
|
-
reserveNo:
|
|
262
|
+
reserveYes: normalizeUsdcRawToUsd(market.reserveYes),
|
|
263
|
+
reserveNo: normalizeUsdcRawToUsd(market.reserveNo),
|
|
256
264
|
totalVolumeUsd: toNumber(market.totalVolume),
|
|
257
265
|
tvlUsd: toNumber(market.currentTvl),
|
|
258
266
|
diagnostics,
|
|
@@ -15,6 +15,7 @@ const tradingClientCache = new Map();
|
|
|
15
15
|
const derivedCredsCache = new Map();
|
|
16
16
|
|
|
17
17
|
function toNumber(value) {
|
|
18
|
+
if (value === null || value === undefined || value === '') return null;
|
|
18
19
|
const numeric = Number(value);
|
|
19
20
|
if (!Number.isFinite(numeric)) return null;
|
|
20
21
|
return numeric;
|
|
@@ -751,7 +752,15 @@ function calculateExecutableDepthUsd(orderbook, side, slippageBps) {
|
|
|
751
752
|
|
|
752
753
|
const limitPriceFactor = slippageBps / 10_000;
|
|
753
754
|
const entries = side === 'buy' ? normalized.asks : normalized.bids;
|
|
754
|
-
const
|
|
755
|
+
const bestBid = normalized.bids.length ? normalized.bids[0].price : null;
|
|
756
|
+
const bestAsk = normalized.asks.length ? normalized.asks[0].price : null;
|
|
757
|
+
const referencePrice =
|
|
758
|
+
side === 'buy'
|
|
759
|
+
? (bestAsk !== null ? bestAsk : mid)
|
|
760
|
+
: (bestBid !== null ? bestBid : mid);
|
|
761
|
+
const priceLimit = side === 'buy'
|
|
762
|
+
? referencePrice * (1 + limitPriceFactor)
|
|
763
|
+
: referencePrice * (1 - limitPriceFactor);
|
|
755
764
|
|
|
756
765
|
let depthUsd = 0;
|
|
757
766
|
let depthShares = 0;
|
|
@@ -770,6 +779,7 @@ function calculateExecutableDepthUsd(orderbook, side, slippageBps) {
|
|
|
770
779
|
depthShares: round(depthShares, 6) || 0,
|
|
771
780
|
worstPrice: worstPrice === null ? null : round(worstPrice, 8),
|
|
772
781
|
midPrice: round(mid, 8),
|
|
782
|
+
referencePrice: round(referencePrice, 8),
|
|
773
783
|
diagnostics: [],
|
|
774
784
|
};
|
|
775
785
|
}
|
|
@@ -852,7 +862,9 @@ async function fetchDepthForMarket(market, options = {}) {
|
|
|
852
862
|
const candidates = [yesDepth && yesDepth.depthUsd, noDepth && noDepth.depthUsd].filter((value) => Number.isFinite(value));
|
|
853
863
|
const minDepthWithinSlippageUsd = candidates.length ? Math.min(...candidates) : 0;
|
|
854
864
|
const bestDepthWithinSlippageUsd = candidates.length ? Math.max(...candidates) : 0;
|
|
855
|
-
|
|
865
|
+
// Keep the legacy/conservative aggregate as min depth (used by sizing paths),
|
|
866
|
+
// while exposing best-depth separately for hedge-side diagnostics.
|
|
867
|
+
const depthWithinSlippageUsd = minDepthWithinSlippageUsd;
|
|
856
868
|
|
|
857
869
|
if (!yesDepth) diagnostics.push('YES token orderbook unavailable.');
|
|
858
870
|
if (!noDepth) diagnostics.push('NO token orderbook unavailable.');
|