@steerprotocol/liquidity-meter 1.0.0 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -56
- package/bin/liquidity-depth.js +795 -0
- package/package.json +42 -32
- package/src/api.js +568 -0
- package/src/depth.js +492 -0
- package/src/index.js +6 -0
- package/src/prices.js +188 -0
- package/src/uniswapv3-subgraph.js +141 -0
- package/src/viem-onchain.js +506 -0
- package/src/withdraw.js +333 -0
- package/templates/README.md +37 -0
- package/templates/pools_minimal.csv +3 -0
- package/templates/pools_with_vault.csv +3 -0
- package/dist/adapters/onchain.d.ts +0 -51
- package/dist/adapters/onchain.js +0 -158
- package/dist/adapters/uniswapv3-subgraph.d.ts +0 -44
- package/dist/adapters/uniswapv3-subgraph.js +0 -105
- package/dist/adapters/withdraw.d.ts +0 -14
- package/dist/adapters/withdraw.js +0 -150
- package/dist/api.d.ts +0 -72
- package/dist/api.js +0 -180
- package/dist/cli/liquidity-depth.d.ts +0 -2
- package/dist/cli/liquidity-depth.js +0 -1160
- package/dist/core/depth.d.ts +0 -48
- package/dist/core/depth.js +0 -314
- package/dist/handlers/cron.d.ts +0 -27
- package/dist/handlers/cron.js +0 -68
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -7
- package/dist/prices.d.ts +0 -15
- package/dist/prices.js +0 -205
- package/dist/wagmi/config.d.ts +0 -2106
- package/dist/wagmi/config.js +0 -24
- package/dist/wagmi/generated.d.ts +0 -2019
- package/dist/wagmi/generated.js +0 -346
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CLI for computing Uniswap v3 market depth bands with real data
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { createPublicClient, parseAbi, getAddress, http as viemHttp } from 'viem';
|
|
8
|
+
import {
|
|
9
|
+
analyzePool,
|
|
10
|
+
findBlockNumbersForTimestamps,
|
|
11
|
+
formatTimestampUTC,
|
|
12
|
+
parseDurationArg,
|
|
13
|
+
parsePercentBuckets,
|
|
14
|
+
parseTimestampArg,
|
|
15
|
+
resolveAnalysisBlockContext,
|
|
16
|
+
} from '../src/api.js';
|
|
17
|
+
import { createForkClient, simulateVaultWithdraw, resolveViemChain } from '../src/withdraw.js';
|
|
18
|
+
|
|
19
|
+
const UNISWAP_V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
|
|
20
|
+
const FACTORY_ABI = parseAbi([
|
|
21
|
+
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = {
|
|
26
|
+
percent: '1,2,5,10',
|
|
27
|
+
subgraph: undefined,
|
|
28
|
+
source: 'tevm',
|
|
29
|
+
rpc: process.env.RPC_URL,
|
|
30
|
+
usdSizes: '1000,5000,10000,25000,50000,100000',
|
|
31
|
+
prices: process.env.PRICES_SOURCE || 'auto',
|
|
32
|
+
reserveLimit: Number(process.env.RESERVE_LIMIT) || 100,
|
|
33
|
+
csv: undefined,
|
|
34
|
+
outdir: 'reports',
|
|
35
|
+
// withdraw simulation
|
|
36
|
+
vault: undefined,
|
|
37
|
+
owner: process.env.OWNER,
|
|
38
|
+
withdrawPct: undefined,
|
|
39
|
+
withdrawShares: undefined,
|
|
40
|
+
block: undefined,
|
|
41
|
+
timestamp: undefined,
|
|
42
|
+
timeStart: undefined,
|
|
43
|
+
timeEnd: undefined,
|
|
44
|
+
timeInterval: undefined,
|
|
45
|
+
timeWindow: undefined,
|
|
46
|
+
timeCount: undefined,
|
|
47
|
+
};
|
|
48
|
+
for (let i = 2; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
if (a === '--pool' || a === '-p') args.pool = argv[++i];
|
|
51
|
+
else if (a === '--percent' || a === '-b') args.percent = argv[++i];
|
|
52
|
+
else if (a === '--subgraph' || a === '-s') args.subgraph = argv[++i];
|
|
53
|
+
else if (a === '--rpc' || a === '-r') args.rpc = argv[++i];
|
|
54
|
+
else if (a === '--source') args.source = argv[++i]; // tevm|subgraph|auto
|
|
55
|
+
else if (a === '--token0-usd') args.token0USD = Number(argv[++i]);
|
|
56
|
+
else if (a === '--token1-usd') args.token1USD = Number(argv[++i]);
|
|
57
|
+
else if (a === '--assume-stable') args.assumeStable = Number(argv[++i]); // 0 or 1
|
|
58
|
+
else if (a === '--usd-sizes') args.usdSizes = argv[++i];
|
|
59
|
+
else if (a === '--prices') args.prices = argv[++i]; // llama|coingecko|auto
|
|
60
|
+
// withdraw simulation flags
|
|
61
|
+
else if (a === '--vault') args.vault = argv[++i];
|
|
62
|
+
else if (a === '--owner') args.owner = argv[++i];
|
|
63
|
+
else if (a === '--withdraw-pct' || a === '--withdraw-percent') args.withdrawPct = Number(argv[++i]);
|
|
64
|
+
else if (a === '--withdraw-shares') args.withdrawShares = argv[++i];
|
|
65
|
+
else if (a === '--block') args.block = argv[++i];
|
|
66
|
+
else if (a === '--timestamp' || a === '--time') args.timestamp = argv[++i];
|
|
67
|
+
else if (a === '--time-start') args.timeStart = argv[++i];
|
|
68
|
+
else if (a === '--time-end') args.timeEnd = argv[++i];
|
|
69
|
+
else if (a === '--time-interval') args.timeInterval = argv[++i];
|
|
70
|
+
else if (a === '--time-window') args.timeWindow = argv[++i];
|
|
71
|
+
else if (a === '--time-count') args.timeCount = Number(argv[++i]);
|
|
72
|
+
else if (a === '--debug') args.debug = true;
|
|
73
|
+
else if (a === '--csv') args.csv = argv[++i];
|
|
74
|
+
else if (a === '--outdir') args.outdir = argv[++i];
|
|
75
|
+
else if (a === '--reserve-limit') args.reserveLimit = Number(argv[++i]);
|
|
76
|
+
else if (a === '--json' || a === '-j') args.json = true;
|
|
77
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
78
|
+
else if (a === '--version' || a === '-v') args.version = true;
|
|
79
|
+
else if (!args.pool && /^0x[0-9a-fA-F]{40}$/.test(a)) args.pool = a;
|
|
80
|
+
else {
|
|
81
|
+
console.error(`Unknown arg: ${a}`);
|
|
82
|
+
args.help = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return args;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printHelp() {
|
|
89
|
+
console.log(`
|
|
90
|
+
liquidity-depth — Uniswap v3 market depth bands
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
liquidity-depth --pool <POOL_ADDRESS> [--percent 1,2,5,10] [--source tevm|viem|subgraph|auto] [--rpc <URL>] [--block N] [--timestamp ISO|SECONDS]
|
|
94
|
+
[--time-start ISO|SECONDS] [--time-end ISO|SECONDS] [--time-interval 1h] [--time-window 7d] [--time-count N]
|
|
95
|
+
[--token0-usd N] [--token1-usd N] [--usd-sizes 1000,5000,...] [--json]
|
|
96
|
+
liquidity-depth --pool <POOL_ADDRESS> --vault <VAULT_ADDRESS> --owner <OWNER_ADDRESS> [--withdraw-pct N | --withdraw-shares <SHARES>] [same options]
|
|
97
|
+
liquidity-depth --csv <FILE.csv> [--outdir reports] [same options as above]
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
-p, --pool Uniswap v3 pool address (e.g., 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8)
|
|
101
|
+
-b, --percent Percent buckets, comma-separated (default: 1,2,5,10)
|
|
102
|
+
-s, --subgraph Subgraph URL (default: Uniswap v3 mainnet)
|
|
103
|
+
--source Data source: tevm|viem|subgraph|auto (default: tevm)
|
|
104
|
+
-r, --rpc RPC URL (for on-chain fetching via tevm fork). Also reads RPC_URL env var
|
|
105
|
+
--token0-usd Override token0 USD price
|
|
106
|
+
--token1-usd Override token1 USD price
|
|
107
|
+
--assume-stable 0|1 Assume token index is $1 stable (fills missing USD)
|
|
108
|
+
--usd-sizes Comma-separated USD sizes for price impact (default: 1000,5000,10000,25000,50000,100000)
|
|
109
|
+
--block Fork/query state at the specified block number
|
|
110
|
+
--timestamp Resolve a historical block by timestamp (seconds or ISO-8601); requires --rpc
|
|
111
|
+
--time-start Beginning timestamp (inclusive) for time-series mode (seconds or ISO-8601)
|
|
112
|
+
--time-end Ending timestamp (inclusive, default: now) for time-series mode
|
|
113
|
+
--time-interval Step between snapshots in time-series mode (e.g., 1h, 30m; default: 1h)
|
|
114
|
+
--time-window How far back from --time-end to start when --time-start omitted (e.g., 7d)
|
|
115
|
+
--time-count Number of intervals to fetch (e.g., 168 for 7 days hourly); implies backwards from --time-end
|
|
116
|
+
--prices Price source: reserve|dexscreener|llama|coingecko|auto (default: auto; env PRICES_SOURCE)
|
|
117
|
+
--reserve-limit N Limit for Reserve API (default: 100; env RESERVE_LIMIT)
|
|
118
|
+
--debug Print debug logs to stderr for pricing/inference
|
|
119
|
+
--csv CSV file with a header including a 'pool' or 'address' column
|
|
120
|
+
--outdir Output directory for per-pool reports (default: reports)
|
|
121
|
+
--vault Steer vault address (to simulate withdrawal)
|
|
122
|
+
--owner Owner address whose shares are withdrawn (also reads OWNER env)
|
|
123
|
+
--withdraw-pct Percent of owner's shares to withdraw (e.g., 25)
|
|
124
|
+
--withdraw-shares Raw share amount to withdraw (overrides --withdraw-pct)
|
|
125
|
+
-j, --json Output machine-readable JSON
|
|
126
|
+
-h, --help Show help
|
|
127
|
+
-v, --version Show version
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function looksLikeAddress(x) {
|
|
132
|
+
return typeof x === 'string' && /^0x[0-9a-fA-F]{40}$/.test(x.trim());
|
|
133
|
+
}
|
|
134
|
+
function extractFirstAddress(x) {
|
|
135
|
+
if (typeof x !== 'string') return undefined;
|
|
136
|
+
const m = x.match(/0x[0-9a-fA-F]{40}/);
|
|
137
|
+
return m ? m[0] : undefined;
|
|
138
|
+
}
|
|
139
|
+
function normKey(k) {
|
|
140
|
+
return String(k || '').toLowerCase().replace(/[ _-]+/g, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Minimal CSV parser: returns array of row objects keyed by header.
|
|
144
|
+
// Supports commas, quoted fields with double-quotes escaping.
|
|
145
|
+
function parseCSV(content) {
|
|
146
|
+
const outRows = [];
|
|
147
|
+
let i = 0;
|
|
148
|
+
const len = content.length;
|
|
149
|
+
const fields = [];
|
|
150
|
+
function readLine() {
|
|
151
|
+
fields.length = 0;
|
|
152
|
+
let field = '';
|
|
153
|
+
let inQuotes = false;
|
|
154
|
+
let sawAny = false;
|
|
155
|
+
while (i < len) {
|
|
156
|
+
const ch = content[i++];
|
|
157
|
+
if (!inQuotes && (ch === '\n' || ch === '\r')) {
|
|
158
|
+
if (ch === '\r' && content[i] === '\n') i++; // CRLF
|
|
159
|
+
fields.push(field);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (ch === '"') {
|
|
163
|
+
if (inQuotes) {
|
|
164
|
+
if (content[i] === '"') { field += '"'; i++; }
|
|
165
|
+
else { inQuotes = false; }
|
|
166
|
+
} else {
|
|
167
|
+
inQuotes = true;
|
|
168
|
+
sawAny = true;
|
|
169
|
+
}
|
|
170
|
+
} else if (ch === ',' && !inQuotes) {
|
|
171
|
+
fields.push(field);
|
|
172
|
+
field = '';
|
|
173
|
+
sawAny = true;
|
|
174
|
+
} else {
|
|
175
|
+
field += ch;
|
|
176
|
+
sawAny = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!sawAny && field === '') return false; // EOF after last newline
|
|
180
|
+
fields.push(field);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
// read header
|
|
184
|
+
if (!readLine()) return [];
|
|
185
|
+
const header = fields.map((h) => h.trim());
|
|
186
|
+
while (readLine()) {
|
|
187
|
+
const obj = {};
|
|
188
|
+
for (let j = 0; j < header.length; j++) obj[header[j]] = (fields[j] ?? '').trim();
|
|
189
|
+
outRows.push(obj);
|
|
190
|
+
}
|
|
191
|
+
return outRows;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runBatch(args) {
|
|
195
|
+
const csvPath = args.csv;
|
|
196
|
+
const outdir = args.outdir || 'reports';
|
|
197
|
+
const data = await fs.readFile(csvPath, 'utf8');
|
|
198
|
+
const rows = parseCSV(data);
|
|
199
|
+
if (!rows.length) {
|
|
200
|
+
console.error('CSV is empty or could not be parsed. Expect a header row with a pool/address column.');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
204
|
+
|
|
205
|
+
const pick = (obj, names) => {
|
|
206
|
+
// try direct keys
|
|
207
|
+
for (const n of names) if (obj[n] != null && String(obj[n]).trim() !== '') return String(obj[n]).trim();
|
|
208
|
+
// try normalized keys
|
|
209
|
+
const map = Object.fromEntries(Object.keys(obj).map((k) => [normKey(k), k]));
|
|
210
|
+
for (const n of names) {
|
|
211
|
+
const key = map[normKey(n)];
|
|
212
|
+
if (key && obj[key] != null && String(obj[key]).trim() !== '') return String(obj[key]).trim();
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function parseFeeToUint24(v) {
|
|
218
|
+
if (v == null) return undefined;
|
|
219
|
+
const s = String(v).trim();
|
|
220
|
+
if (!s) return undefined;
|
|
221
|
+
// raw number like 500, 3000, 10000
|
|
222
|
+
if (/^\d+$/.test(s)) {
|
|
223
|
+
const n = Number(s);
|
|
224
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
225
|
+
}
|
|
226
|
+
// like 30bps or 5 bps
|
|
227
|
+
const mBps = s.match(/([0-9]+(?:\.[0-9]+)?)\s*bps/i);
|
|
228
|
+
if (mBps) {
|
|
229
|
+
const n = Number(mBps[1]);
|
|
230
|
+
if (Number.isFinite(n)) return Math.round(n * 100);
|
|
231
|
+
}
|
|
232
|
+
// like 0.3% or 1 %
|
|
233
|
+
const mPct = s.match(/([0-9]+(?:\.[0-9]+)?)\s*%/);
|
|
234
|
+
if (mPct) {
|
|
235
|
+
const n = Number(mPct[1]);
|
|
236
|
+
if (Number.isFinite(n)) return Math.round(n * 1e4);
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function resolvePoolFromTokens(row, rpcUrl) {
|
|
242
|
+
// attempt to find token addresses + fee
|
|
243
|
+
let t0 = pick(row, ['token0', 'token0_address', 'token 0', 'token 0 address', 'tokenA', 'base']);
|
|
244
|
+
let t1 = pick(row, ['token1', 'token1_address', 'token 1', 'token 1 address', 'tokenB', 'quote']);
|
|
245
|
+
let fee = pick(row, ['fee', 'fee_tier', 'fee tier', 'feebps']);
|
|
246
|
+
|
|
247
|
+
// If not found, scan all values for first two distinct addresses
|
|
248
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '')) {
|
|
249
|
+
const found = [];
|
|
250
|
+
for (const v of Object.values(row)) {
|
|
251
|
+
const addr = extractFirstAddress(String(v || ''));
|
|
252
|
+
if (addr && !found.includes(addr)) found.push(addr);
|
|
253
|
+
if (found.length >= 2) break;
|
|
254
|
+
}
|
|
255
|
+
if (!looksLikeAddress(t0 || '') && found[0]) t0 = found[0];
|
|
256
|
+
if (!looksLikeAddress(t1 || '') && found[1]) t1 = found[1];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// parse fee
|
|
260
|
+
const feeNum = parseFeeToUint24(fee);
|
|
261
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '') || !feeNum) return undefined;
|
|
262
|
+
if (!rpcUrl) return undefined;
|
|
263
|
+
|
|
264
|
+
const client = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
265
|
+
try {
|
|
266
|
+
const pool = await client.readContract({ address: UNISWAP_V3_FACTORY, abi: FACTORY_ABI, functionName: 'getPool', args: [getAddress(t0), getAddress(t1), feeNum] });
|
|
267
|
+
if (pool && pool !== '0x0000000000000000000000000000000000000000') return pool;
|
|
268
|
+
} catch (_) {
|
|
269
|
+
// ignore
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (let idx = 0; idx < rows.length; idx++) {
|
|
275
|
+
const row = rows[idx];
|
|
276
|
+
let pool = pick(row, ['pool', 'address', 'pool_address', 'id', 'Pool', 'Address', 'contract_address', 'contract address', 'pair_address', 'pair address']);
|
|
277
|
+
if (!looksLikeAddress(pool || '')) {
|
|
278
|
+
// try extract from any cell (e.g., Etherscan/Uniswap URL)
|
|
279
|
+
for (const v of Object.values(row)) {
|
|
280
|
+
const found = extractFirstAddress(String(v || ''));
|
|
281
|
+
if (found) { pool = found; break; }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!looksLikeAddress(pool || '')) {
|
|
285
|
+
// try factory lookup using tokens + fee
|
|
286
|
+
const viaFactory = await resolvePoolFromTokens(row, args.rpc);
|
|
287
|
+
if (viaFactory) pool = viaFactory;
|
|
288
|
+
}
|
|
289
|
+
if (!looksLikeAddress(pool || '')) {
|
|
290
|
+
console.warn(`Row ${idx + 1}: skipping (no valid pool address)`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rowArgs = {
|
|
295
|
+
...args,
|
|
296
|
+
pool,
|
|
297
|
+
rpc: pick(row, ['rpc', 'RPC', 'rpc_url', 'RPC_URL']) || args.rpc,
|
|
298
|
+
source: pick(row, ['source', 'Source']) || args.source,
|
|
299
|
+
percent: pick(row, ['percent', 'Percents', 'buckets']) || args.percent,
|
|
300
|
+
block: pick(row, ['block', 'Block', 'block_number', 'block number']) || args.block,
|
|
301
|
+
timestamp: pick(row, ['timestamp', 'Timestamp', 'time', 'Time']) || args.timestamp,
|
|
302
|
+
token0USD: pick(row, ['token0_usd', 'token0-usd', 'token0USD']) != null ? Number(pick(row, ['token0_usd', 'token0-usd', 'token0USD'])) : args.token0USD,
|
|
303
|
+
token1USD: pick(row, ['token1_usd', 'token1-usd', 'token1USD']) != null ? Number(pick(row, ['token1_usd', 'token1-usd', 'token1USD'])) : args.token1USD,
|
|
304
|
+
usdSizes: pick(row, ['usd_sizes', 'usd-sizes', 'usdSizes']) || args.usdSizes,
|
|
305
|
+
assumeStable: pick(row, ['assume_stable', 'assume-stable', 'assumeStable']) != null ? Number(pick(row, ['assume_stable', 'assume-stable', 'assumeStable'])) : args.assumeStable,
|
|
306
|
+
prices: pick(row, ['prices', 'Prices']) || args.prices,
|
|
307
|
+
vault: pick(row, ['vault', 'Vault', 'vault_address', 'VaultAddress', 'vault address', 'strategy']) || args.vault,
|
|
308
|
+
owner: pick(row, ['owner', 'Owner', 'owner_address', 'owner address', 'withdraw_owner', 'withdraw owner', 'account', 'user', 'recipient', 'to']) || args.owner,
|
|
309
|
+
withdrawPct: pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw']) != null ? Number(pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw'])) : args.withdrawPct,
|
|
310
|
+
withdrawShares: pick(row, ['withdraw_shares', 'withdraw-shares', 'withdraw shares', 'withdrawShares', 'shares_to_withdraw', 'withdraw_share_amount']) || args.withdrawShares,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const wpct = rowArgs.withdrawPct;
|
|
314
|
+
const wshares = rowArgs.withdrawShares;
|
|
315
|
+
const vault = rowArgs.vault;
|
|
316
|
+
const owner = rowArgs.owner;
|
|
317
|
+
|
|
318
|
+
if (args.debug) {
|
|
319
|
+
const dbg = [];
|
|
320
|
+
if (vault) dbg.push(`vault=${vault}`);
|
|
321
|
+
if (owner) dbg.push(`owner=${owner}`);
|
|
322
|
+
if (wpct) dbg.push(`withdraw_pct=${wpct}`);
|
|
323
|
+
if (wshares) dbg.push(`withdraw_shares=${wshares}`);
|
|
324
|
+
if (dbg.length) console.error(`[batch] row ${idx + 1} overrides: ${dbg.join(' ')}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const baseName = `${pool.toLowerCase()}`;
|
|
329
|
+
const file = path.join(outdir, `${baseName}.${args.json ? 'json' : 'txt'}`);
|
|
330
|
+
const output = await runSingleInvocation(rowArgs);
|
|
331
|
+
await fs.writeFile(file, output, 'utf8');
|
|
332
|
+
console.log(`Wrote: ${file}`);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const errMsg = String(err?.stack || err?.message || err);
|
|
335
|
+
const errFile = path.join(outdir, `error-${pool.toLowerCase()}.txt`);
|
|
336
|
+
await fs.writeFile(errFile, errMsg, 'utf8');
|
|
337
|
+
console.error(`Failed row ${idx + 1} (${pool}): ${err?.message || err}`);
|
|
338
|
+
console.error(` -> see ${errFile}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function runTimeSeries(args) {
|
|
344
|
+
if (!args.pool) {
|
|
345
|
+
console.error('Time-series mode requires --pool');
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
if (args.block) {
|
|
349
|
+
console.error('Time-series mode cannot be combined with --block. Use --time-start/--time-window/--time-count with --timestamp.');
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
if (!args.rpc) {
|
|
353
|
+
console.error('Time-series mode requires --rpc (or RPC_URL env) so timestamps can be resolved to blocks.');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let intervalSec;
|
|
358
|
+
try {
|
|
359
|
+
intervalSec = parseDurationArg(args.timeInterval ?? '1h');
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.error(`Invalid --time-interval value: ${e?.message || e}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
if (intervalSec <= 0n) {
|
|
365
|
+
console.error('--time-interval must be greater than zero');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let endSec;
|
|
370
|
+
try {
|
|
371
|
+
endSec = args.timeEnd != null ? parseTimestampArg(args.timeEnd) : BigInt(Math.floor(Date.now() / 1000));
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(`Invalid --time-end value: ${e?.message || e}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let startSec;
|
|
378
|
+
if (args.timeStart != null) {
|
|
379
|
+
try {
|
|
380
|
+
startSec = parseTimestampArg(args.timeStart);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.error(`Invalid --time-start value: ${e?.message || e}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (startSec == null && args.timeWindow != null) {
|
|
387
|
+
try {
|
|
388
|
+
const windowSec = parseDurationArg(args.timeWindow);
|
|
389
|
+
startSec = endSec > windowSec ? endSec - windowSec : 0n;
|
|
390
|
+
} catch (e) {
|
|
391
|
+
console.error(`Invalid --time-window value: ${e?.message || e}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let count = undefined;
|
|
397
|
+
if (args.timeCount != null) {
|
|
398
|
+
if (!Number.isFinite(args.timeCount) || args.timeCount <= 0) {
|
|
399
|
+
console.error('--time-count must be a positive integer');
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
count = Math.floor(args.timeCount);
|
|
403
|
+
}
|
|
404
|
+
if (startSec == null) {
|
|
405
|
+
if (count != null) {
|
|
406
|
+
const span = intervalSec * BigInt(Math.max(count - 1, 0));
|
|
407
|
+
startSec = endSec > span ? endSec - span : 0n;
|
|
408
|
+
} else {
|
|
409
|
+
console.error('Time-series mode requires --time-start, --time-window, or --time-count');
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (startSec > endSec) {
|
|
415
|
+
[startSec, endSec] = [endSec, startSec];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const timestamps = [];
|
|
419
|
+
const maxPoints = 10000;
|
|
420
|
+
for (let ts = startSec; ts <= endSec; ts += intervalSec) {
|
|
421
|
+
timestamps.push(ts);
|
|
422
|
+
if (count != null && timestamps.length >= count) break;
|
|
423
|
+
if (timestamps.length > maxPoints) {
|
|
424
|
+
console.error(`Time-series would produce more than ${maxPoints} snapshots. Increase --time-interval or reduce the window/count.`);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!timestamps.length) timestamps.push(startSec);
|
|
429
|
+
if (count == null && timestamps[timestamps.length - 1] < endSec) {
|
|
430
|
+
timestamps.push(endSec);
|
|
431
|
+
}
|
|
432
|
+
if (count != null && timestamps.length > count) {
|
|
433
|
+
timestamps.splice(0, timestamps.length - count);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tsStrings = timestamps.map((ts) => ts.toString());
|
|
437
|
+
if (!tsStrings.length) {
|
|
438
|
+
console.error('No timestamps resolved for time-series mode');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const resolvedBlocks = await findBlockNumbersForTimestamps({
|
|
442
|
+
rpcUrl: args.rpc,
|
|
443
|
+
timestamps,
|
|
444
|
+
debug: !!args.debug,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!args.json) {
|
|
448
|
+
const startIso = formatTimestampUTC(BigInt(tsStrings[0]));
|
|
449
|
+
const endIso = formatTimestampUTC(BigInt(tsStrings[tsStrings.length - 1]));
|
|
450
|
+
const intervalHuman = args.timeInterval ?? '1h';
|
|
451
|
+
console.log(`Time-series: ${tsStrings.length} snapshots from ${startIso || tsStrings[0]} to ${endIso || tsStrings[tsStrings.length - 1]} every ~${intervalHuman}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const jsonResults = [];
|
|
455
|
+
for (let i = 0; i < tsStrings.length; i++) {
|
|
456
|
+
const tsStr = tsStrings[i];
|
|
457
|
+
const pointArgs = {
|
|
458
|
+
...args,
|
|
459
|
+
timestamp: undefined,
|
|
460
|
+
block: resolvedBlocks[i].toString(),
|
|
461
|
+
};
|
|
462
|
+
const label = formatTimestampUTC(BigInt(tsStr)) || tsStr;
|
|
463
|
+
if (!args.json) {
|
|
464
|
+
console.log(`\n=== ${label} (unix ${tsStr}) ===`);
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const output = await runSingleInvocation(pointArgs);
|
|
468
|
+
if (args.json) {
|
|
469
|
+
const text = output.trim();
|
|
470
|
+
if (text) {
|
|
471
|
+
jsonResults.push(JSON.parse(text));
|
|
472
|
+
} else {
|
|
473
|
+
jsonResults.push(null);
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
process.stdout.write(output);
|
|
477
|
+
if (!output.endsWith('\n')) console.log('');
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(`Time-series run failed for timestamp ${tsStr}: ${err?.message || err}`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (args.json) {
|
|
486
|
+
console.log(JSON.stringify(jsonResults, null, 2));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function captureConsoleOutput(run) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
const original = console.log;
|
|
493
|
+
console.log = (...args) => {
|
|
494
|
+
lines.push(args.join(' '));
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
run();
|
|
498
|
+
} finally {
|
|
499
|
+
console.log = original;
|
|
500
|
+
}
|
|
501
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function executeSingle(args) {
|
|
505
|
+
let percentBuckets;
|
|
506
|
+
try {
|
|
507
|
+
percentBuckets = parsePercentBuckets(args.percent);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
throw new Error(`Invalid --percent value: ${e?.message || e}`);
|
|
510
|
+
}
|
|
511
|
+
if (!percentBuckets.length) {
|
|
512
|
+
throw new Error('No valid percent buckets parsed.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const wantsWithdraw =
|
|
516
|
+
args.vault && (Number.isFinite(args.withdrawPct) || args.withdrawShares);
|
|
517
|
+
let sharedClient = null;
|
|
518
|
+
let sharedBlockTag;
|
|
519
|
+
if (wantsWithdraw) {
|
|
520
|
+
if (!args.rpc) {
|
|
521
|
+
throw new Error('Missing --rpc (or RPC_URL env) for tevm source.');
|
|
522
|
+
}
|
|
523
|
+
const blockContext = await resolveAnalysisBlockContext({
|
|
524
|
+
block: args.block,
|
|
525
|
+
timestamp: args.timestamp,
|
|
526
|
+
rpcUrl: args.rpc,
|
|
527
|
+
debug: args.debug,
|
|
528
|
+
});
|
|
529
|
+
sharedBlockTag = blockContext.blockTag;
|
|
530
|
+
sharedClient = await createForkClient({
|
|
531
|
+
rpcUrl: args.rpc,
|
|
532
|
+
blockTag: sharedBlockTag,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let before;
|
|
537
|
+
before = await analyzePool({
|
|
538
|
+
poolAddress: args.pool,
|
|
539
|
+
percentBuckets,
|
|
540
|
+
source: args.source,
|
|
541
|
+
rpcUrl: args.rpc,
|
|
542
|
+
subgraphUrl: args.subgraph,
|
|
543
|
+
token0USD: args.token0USD,
|
|
544
|
+
token1USD: args.token1USD,
|
|
545
|
+
assumeStable: args.assumeStable,
|
|
546
|
+
usdSizes: args.usdSizes,
|
|
547
|
+
prices: args.prices,
|
|
548
|
+
reserveLimit: args.reserveLimit,
|
|
549
|
+
block: args.block,
|
|
550
|
+
timestamp: args.timestamp,
|
|
551
|
+
debug: args.debug,
|
|
552
|
+
client: sharedClient,
|
|
553
|
+
blockTag: sharedBlockTag,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
let after = null;
|
|
557
|
+
if (before.usedSource === 'tevm' && wantsWithdraw) {
|
|
558
|
+
const viemChain = await resolveViemChain(args.rpc);
|
|
559
|
+
await simulateVaultWithdraw({
|
|
560
|
+
client: before.client,
|
|
561
|
+
vault: args.vault,
|
|
562
|
+
owner: args.owner,
|
|
563
|
+
withdrawPct: args.withdrawPct,
|
|
564
|
+
withdrawShares: args.withdrawShares,
|
|
565
|
+
debug: !!args.debug,
|
|
566
|
+
cliPool: args.pool,
|
|
567
|
+
viemChain,
|
|
568
|
+
});
|
|
569
|
+
after = await analyzePool({
|
|
570
|
+
poolAddress: args.pool,
|
|
571
|
+
percentBuckets,
|
|
572
|
+
source: 'tevm',
|
|
573
|
+
rpcUrl: args.rpc,
|
|
574
|
+
token0USD: args.token0USD,
|
|
575
|
+
token1USD: args.token1USD,
|
|
576
|
+
assumeStable: args.assumeStable,
|
|
577
|
+
usdSizes: args.usdSizes,
|
|
578
|
+
prices: args.prices,
|
|
579
|
+
reserveLimit: args.reserveLimit,
|
|
580
|
+
debug: args.debug,
|
|
581
|
+
client: before.client,
|
|
582
|
+
blockTag: before.blockTag,
|
|
583
|
+
});
|
|
584
|
+
if (args.debug) {
|
|
585
|
+
console.error(`[after] tick: ${before.snap.tick} -> ${after.snap.tick}`);
|
|
586
|
+
console.error(
|
|
587
|
+
`[after] liquidity: ${before.snap.liquidity.toString()} -> ${after.snap.liquidity.toString()}`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return { before, after };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function renderSingle(args, before, after) {
|
|
596
|
+
if (args.json) {
|
|
597
|
+
const output = after
|
|
598
|
+
? {
|
|
599
|
+
before: serializeAnalysis(args, before),
|
|
600
|
+
after: serializeAnalysis(args, after),
|
|
601
|
+
}
|
|
602
|
+
: serializeAnalysis(args, before);
|
|
603
|
+
return `${JSON.stringify(output, null, 2)}\n`;
|
|
604
|
+
}
|
|
605
|
+
return captureConsoleOutput(() => {
|
|
606
|
+
printAnalysisSummary(args, before);
|
|
607
|
+
if (after) {
|
|
608
|
+
console.log('');
|
|
609
|
+
console.log('--- After Withdraw ---');
|
|
610
|
+
printAnalysisSummary(args, after);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function runSingleInvocation(args) {
|
|
616
|
+
const { before, after } = await executeSingle(args);
|
|
617
|
+
return renderSingle(args, before, after);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function main() {
|
|
621
|
+
const args = parseArgs(process.argv);
|
|
622
|
+
if (args.version) {
|
|
623
|
+
console.log('liquidity-depth 0.1.0');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (args.help || (!args.pool && !args.csv)) {
|
|
627
|
+
printHelp();
|
|
628
|
+
if (!args.pool && !args.csv) process.exitCode = 1;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (args.csv) {
|
|
633
|
+
await runBatch(args);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const wantsTimeSeries =
|
|
638
|
+
args.timeStart != null ||
|
|
639
|
+
args.timeEnd != null ||
|
|
640
|
+
args.timeInterval != null ||
|
|
641
|
+
args.timeWindow != null ||
|
|
642
|
+
args.timeCount != null;
|
|
643
|
+
if (wantsTimeSeries) {
|
|
644
|
+
await runTimeSeries(args);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const output = await runSingleInvocation(args);
|
|
650
|
+
process.stdout.write(output);
|
|
651
|
+
} catch (e) {
|
|
652
|
+
if (
|
|
653
|
+
args.timestamp != null &&
|
|
654
|
+
args.rpc == null &&
|
|
655
|
+
String(e?.message || '').includes('--timestamp requires --rpc')
|
|
656
|
+
) {
|
|
657
|
+
console.error('--timestamp requires --rpc (or RPC_URL env) to resolve block number');
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
throw e;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function serializeAnalysis(args, analysis) {
|
|
665
|
+
const snap = analysis.snap;
|
|
666
|
+
const blockNumber =
|
|
667
|
+
snap.meta?.blockNumber ?? (analysis.blockNumber !== undefined ? analysis.blockNumber : undefined);
|
|
668
|
+
const timestamp =
|
|
669
|
+
snap.meta?.blockTimestamp ??
|
|
670
|
+
(analysis.timestampSec !== undefined ? analysis.timestampSec : undefined);
|
|
671
|
+
return {
|
|
672
|
+
pool: snap.meta.poolId,
|
|
673
|
+
tokens: {
|
|
674
|
+
token0: snap.meta.token0,
|
|
675
|
+
token1: snap.meta.token1,
|
|
676
|
+
},
|
|
677
|
+
ethPriceUSD: snap.meta.ethPriceUSD,
|
|
678
|
+
tick: snap.tick,
|
|
679
|
+
sqrtPriceX96: snap.sqrtPriceX96.toString(),
|
|
680
|
+
liquidity: snap.liquidity.toString(),
|
|
681
|
+
feePips: snap.feePips,
|
|
682
|
+
range: snap.meta.range,
|
|
683
|
+
percentBuckets: analysis.percentBuckets,
|
|
684
|
+
block: {
|
|
685
|
+
number: blockNumber != null ? String(blockNumber) : undefined,
|
|
686
|
+
timestamp: timestamp != null ? Number(timestamp) : undefined,
|
|
687
|
+
iso: formatTimestampUTC(timestamp),
|
|
688
|
+
input:
|
|
689
|
+
args.block ??
|
|
690
|
+
args.timestamp ??
|
|
691
|
+
(typeof analysis.blockTag === 'string' ? analysis.blockTag : undefined),
|
|
692
|
+
},
|
|
693
|
+
...analysis.result,
|
|
694
|
+
priceImpacts: analysis.priceImpacts
|
|
695
|
+
? {
|
|
696
|
+
usdSizes: analysis.usdSizes,
|
|
697
|
+
buyPct: analysis.priceImpacts.buyPct,
|
|
698
|
+
sellPct: analysis.priceImpacts.sellPct,
|
|
699
|
+
}
|
|
700
|
+
: undefined,
|
|
701
|
+
prices: {
|
|
702
|
+
token0: {
|
|
703
|
+
address: snap.meta.token0.id,
|
|
704
|
+
symbol: snap.meta.token0.symbol,
|
|
705
|
+
usd: analysis.token0USD,
|
|
706
|
+
source: analysis.priceSource0,
|
|
707
|
+
},
|
|
708
|
+
token1: {
|
|
709
|
+
address: snap.meta.token1.id,
|
|
710
|
+
symbol: snap.meta.token1.symbol,
|
|
711
|
+
usd: analysis.token1USD,
|
|
712
|
+
source: analysis.priceSource1,
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
buyCumToken1: analysis.result.buyCumToken1.map((value) => value.toString()),
|
|
716
|
+
sellCumToken0: analysis.result.sellCumToken0.map((value) => value.toString()),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function printAnalysisSummary(args, analysis) {
|
|
721
|
+
const snap = analysis.snap;
|
|
722
|
+
const result = analysis.result;
|
|
723
|
+
const fmtUSD = (value) =>
|
|
724
|
+
new Intl.NumberFormat('en-US', {
|
|
725
|
+
style: 'currency',
|
|
726
|
+
currency: 'USD',
|
|
727
|
+
maximumFractionDigits: 0,
|
|
728
|
+
}).format(value);
|
|
729
|
+
|
|
730
|
+
console.log(`Pool: ${args.pool.toLowerCase()} [source: ${analysis.usedSource}]`);
|
|
731
|
+
const blockNumber =
|
|
732
|
+
snap.meta?.blockNumber ?? (analysis.blockNumber !== undefined ? analysis.blockNumber : undefined);
|
|
733
|
+
const timestamp =
|
|
734
|
+
snap.meta?.blockTimestamp ??
|
|
735
|
+
(analysis.timestampSec !== undefined ? analysis.timestampSec : undefined);
|
|
736
|
+
const blockInfoParts = [];
|
|
737
|
+
if (blockNumber != null) blockInfoParts.push(`#${String(blockNumber)}`);
|
|
738
|
+
const isoTimestamp = formatTimestampUTC(timestamp);
|
|
739
|
+
if (isoTimestamp) blockInfoParts.push(isoTimestamp);
|
|
740
|
+
if (blockNumber == null && typeof analysis.blockTag === 'string') {
|
|
741
|
+
blockInfoParts.push(analysis.blockTag);
|
|
742
|
+
}
|
|
743
|
+
if (blockInfoParts.length) console.log(`Block: ${blockInfoParts.join(' | ')}`);
|
|
744
|
+
console.log(
|
|
745
|
+
`Tokens: ${snap.meta.token0.symbol} (${snap.meta.token0.id}) / ${snap.meta.token1.symbol} (${snap.meta.token1.id})`
|
|
746
|
+
);
|
|
747
|
+
if (snap.meta?.ethPriceUSD) console.log(`ETH/USD: ${snap.meta.ethPriceUSD.toFixed(2)}`);
|
|
748
|
+
const feePct = snap.feePips / 10_000;
|
|
749
|
+
const feeBps = snap.feePips / 100;
|
|
750
|
+
console.log(`Tick: ${snap.tick} Fee: ${feePct.toFixed(2)}% (${feeBps.toFixed(2)} bps)`);
|
|
751
|
+
if (analysis.token0USD || analysis.token1USD) {
|
|
752
|
+
const fmtPrice = (value) =>
|
|
753
|
+
value
|
|
754
|
+
? `$${value.toLocaleString(undefined, { maximumFractionDigits: 6 })}`
|
|
755
|
+
: 'n/a';
|
|
756
|
+
console.log(
|
|
757
|
+
`Prices: ${snap.meta.token0.symbol} ${fmtPrice(analysis.token0USD)} (${analysis.priceSource0 || 'n/a'}), ${snap.meta.token1.symbol} ${fmtPrice(analysis.token1USD)} (${analysis.priceSource1 || 'n/a'})`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
console.log(`Range: [${snap.meta.range.start}, ${snap.meta.range.end}] for ±${snap.meta.farPercent}%`);
|
|
761
|
+
console.log('');
|
|
762
|
+
|
|
763
|
+
const labels = analysis.percentBuckets.map((percent) => `${percent}%`);
|
|
764
|
+
console.log('Buy cumulative (token1 in → price up):');
|
|
765
|
+
labels.forEach((label, index) =>
|
|
766
|
+
console.log(` +${label.padEnd(4)}: ${fmtUSD(result.buyCumUSD[index])}`)
|
|
767
|
+
);
|
|
768
|
+
console.log('Sell cumulative (token0 in → price down):');
|
|
769
|
+
labels.forEach((label, index) =>
|
|
770
|
+
console.log(` -${label.padEnd(4)}: ${fmtUSD(result.sellCumUSD[index])}`)
|
|
771
|
+
);
|
|
772
|
+
console.log('');
|
|
773
|
+
console.log(`Headline Depth +2% : ${fmtUSD(result.depthPlus2USD)}`);
|
|
774
|
+
console.log(`Headline Depth -2% : ${fmtUSD(result.depthMinus2USD)}`);
|
|
775
|
+
|
|
776
|
+
if (!analysis.priceImpacts && analysis.usdSizes.length) {
|
|
777
|
+
console.log('');
|
|
778
|
+
console.log('Note: USD sizes provided but missing USD prices.');
|
|
779
|
+
console.log(' Pass --token0-usd and --token1-usd to enable price impact output.');
|
|
780
|
+
} else if (analysis.priceImpacts) {
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log('Price impact by USD size:');
|
|
783
|
+
analysis.usdSizes.forEach((usd, index) => {
|
|
784
|
+
const buy = analysis.priceImpacts.buyPct[index];
|
|
785
|
+
const sell = analysis.priceImpacts.sellPct[index];
|
|
786
|
+
const fmtPct = (value) => `${value >= 0 ? '+' : ''}${value.toFixed(5)}%`;
|
|
787
|
+
console.log(` $${usd.toLocaleString()}: buy ${fmtPct(buy)} sell ${fmtPct(sell)}`);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
main().catch((err) => {
|
|
793
|
+
console.error(err?.stack || err?.message || String(err));
|
|
794
|
+
process.exit(1);
|
|
795
|
+
});
|