From ac33eca4493d3449b88e50bcabd75879bb4d628e Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 10 Dec 2025 14:19:04 +0200 Subject: [PATCH 1/5] Improve context managment of process, truncate outputs and allow offsets and lengths and reading from the end of outputs --- package-lock.json | 11 +- src/server.ts | 42 ++-- src/terminal-manager.ts | 221 ++++++++++++++++-- src/tools/improved-process-tools.ts | 348 ++++++++++------------------ src/tools/schemas.ts | 2 + src/types.ts | 3 +- test/test-process-pagination.js | 268 +++++++++++++++++++++ 7 files changed, 633 insertions(+), 262 deletions(-) create mode 100644 test/test-process-pagination.js diff --git a/package-lock.json b/package-lock.json index ecbeca63..a9b57e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1865,6 +1865,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2484,6 +2485,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3733,7 +3735,8 @@ "version": "0.0.1521046", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dir-glob": { "version": "3.0.1", @@ -10097,6 +10100,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10433,6 +10437,7 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10811,6 +10816,7 @@ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -10858,6 +10864,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -10978,6 +10985,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11463,6 +11471,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/server.ts b/src/server.ts index 0d27fef9..b48b63cf 100644 --- a/src/server.ts +++ b/src/server.ts @@ -780,35 +780,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "read_process_output", description: ` - Read output from a running process with intelligent completion detection. + Read output from a running process with file-like pagination support. - Automatically detects when process is ready for more input instead of timing out. + Supports partial output reading with offset and length parameters (like read_file): + - 'offset' (start line, default: 0) + * offset=0: Read NEW output since last read (default, like old behavior) + * Positive: Read from absolute line position + * Negative: Read last N lines from end (tail behavior) + - 'length' (max lines to read, default: configurable via 'fileReadLineLimit' setting) - SMART FEATURES: - - Early exit when REPL shows prompt (>>>, >, etc.) - - Detects process completion vs still running - - Prevents hanging on interactive prompts - - Clear status messages about process state + Examples: + - offset: 0, length: 100 → First 100 NEW lines since last read + - offset: 0 → All new lines (respects config limit) + - offset: 500, length: 50 → Lines 500-549 (absolute position) + - offset: -20 → Last 20 lines (tail) + - offset: -100, length: 50 → Last 100 lines (length ignored for tail) + + OUTPUT PROTECTION: + - Uses same fileReadLineLimit as read_file (default: 1000 lines) + - Returns status like: [Reading 100 lines from line 0 (total: 5000 lines, 4900 remaining)] + - Prevents context overflow from verbose processes - REPL USAGE: - - Stops immediately when REPL prompt detected - - Shows clear status: waiting for input vs finished - - Shorter timeouts needed due to smart detection - - Works with Python, Node.js, R, Julia, etc. + SMART FEATURES: + - For offset=0, waits up to timeout_ms for new output to arrive + - Detects REPL prompts and process completion + - Shows process state (waiting for input, finished, etc.) DETECTION STATES: Process waiting for input (ready for interact_with_process) Process finished execution Timeout reached (may still be running) - PERFORMANCE DEBUGGING (verbose_timing parameter): - Set verbose_timing: true to get detailed timing information including: - - Exit reason (early_exit_quick_pattern, early_exit_periodic_check, process_finished, timeout) - - Total duration and time to first output - - Complete timeline of all output events with timestamps - - Which detection mechanism triggered early exit - Use this to identify when timeouts could be reduced or detection patterns improved. - ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema), annotations: { diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index c6cb7d74..ed981795 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -8,12 +8,24 @@ import { analyzeProcessState } from './utils/process-detection.js'; interface CompletedSession { pid: number; - output: string; + outputLines: string[]; // Line-based buffer (consistent with active sessions) exitCode: number | null; startTime: Date; endTime: Date; } +// Result type for paginated output reading +export interface PaginatedOutputResult { + lines: string[]; + totalLines: number; + readFrom: number; // Starting line of this read + readCount: number; // Number of lines returned + remaining: number; // Lines remaining after this read + isComplete: boolean; // Whether process has finished + exitCode?: number | null; // Exit code if completed + runtimeMs?: number; // Runtime in milliseconds (for completed processes) +} + /** * Configuration for spawning a shell with appropriate flags */ @@ -188,7 +200,8 @@ export class TerminalManager { const session: TerminalSession = { pid: childProcess.pid, process: childProcess, - lastOutput: '', + outputLines: [], // Line-based buffer + lastReadIndex: 0, // Track where "new" output starts isBlocked: false, startTime: new Date() }; @@ -240,7 +253,8 @@ export class TerminalManager { lastOutputTime = now; output += text; - session.lastOutput += text; + // Append to line-based buffer + this.appendToLineBuffer(session, text); // Record output event if collecting timing if (collectTiming) { @@ -278,7 +292,8 @@ export class TerminalManager { lastOutputTime = now; output += text; - session.lastOutput += text; + // Append to line-based buffer + this.appendToLineBuffer(session, text); // Record output event if collecting timing if (collectTiming) { @@ -324,7 +339,7 @@ export class TerminalManager { // Store completed session before removing active session this.completedSessions.set(childProcess.pid, { pid: childProcess.pid, - output: output, // Use only the main output variable + outputLines: [...session.outputLines], // Copy line buffer exitCode: code, startTime: session.startTime, endTime: new Date() @@ -348,32 +363,208 @@ export class TerminalManager { }); } - getNewOutput(pid: number): string | null { + /** + * Append text to a session's line buffer + * Handles partial lines and newline splitting + */ + private appendToLineBuffer(session: TerminalSession, text: string): void { + if (!text) return; + + // Split text into lines, keeping track of whether text ends with newline + const lines = text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const isLastFragment = i === lines.length - 1; + const endsWithNewline = text.endsWith('\n'); + + if (session.outputLines.length === 0) { + // First line ever + session.outputLines.push(line); + } else if (i === 0) { + // First fragment - append to last line (might be partial) + session.outputLines[session.outputLines.length - 1] += line; + } else { + // Subsequent lines - add as new lines + session.outputLines.push(line); + } + } + } + + /** + * Read process output with pagination (like file reading) + * @param pid Process ID + * @param offset Line offset: 0=from lastReadIndex, positive=absolute, negative=tail + * @param length Max lines to return + * @param updateReadIndex Whether to update lastReadIndex (default: true for offset=0) + */ + readOutputPaginated(pid: number, offset: number = 0, length: number = 1000): PaginatedOutputResult | null { // First check active sessions const session = this.sessions.get(pid); if (session) { - const output = session.lastOutput; - session.lastOutput = ''; - return output; + return this.readFromLineBuffer( + session.outputLines, + offset, + length, + session.lastReadIndex, + (newIndex) => { session.lastReadIndex = newIndex; }, + false, + undefined + ); } // Then check completed sessions const completedSession = this.completedSessions.get(pid); if (completedSession) { - // Format with output first, then completion info - const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000; - const output = completedSession.output.trim(); - + const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime(); + return this.readFromLineBuffer( + completedSession.outputLines, + offset, + length, + 0, // Completed sessions don't track read position + () => {}, // No-op for completed sessions + true, + completedSession.exitCode, + runtimeMs + ); + } + + return null; + } + + /** + * Internal helper to read from a line buffer with offset/length + */ + private readFromLineBuffer( + lines: string[], + offset: number, + length: number, + lastReadIndex: number, + updateLastRead: (index: number) => void, + isComplete: boolean, + exitCode?: number | null, + runtimeMs?: number + ): PaginatedOutputResult { + const totalLines = lines.length; + let startIndex: number; + let linesToRead: string[]; + + if (offset < 0) { + // Negative offset = tail behavior (read last N lines) + const tailCount = Math.abs(offset); + startIndex = Math.max(0, totalLines - tailCount); + linesToRead = lines.slice(startIndex); + // Don't update lastReadIndex for tail reads + } else if (offset === 0) { + // offset=0 means "from where I last read" (like getNewOutput) + startIndex = lastReadIndex; + linesToRead = lines.slice(startIndex, startIndex + length); + // Update lastReadIndex for "new output" behavior + updateLastRead(Math.min(startIndex + linesToRead.length, totalLines)); + } else { + // Positive offset = absolute position + startIndex = offset; + linesToRead = lines.slice(startIndex, startIndex + length); + // Don't update lastReadIndex for absolute position reads + } + + const readCount = linesToRead.length; + const endIndex = startIndex + readCount; + const remaining = Math.max(0, totalLines - endIndex); + + return { + lines: linesToRead, + totalLines, + readFrom: startIndex, + readCount, + remaining, + isComplete, + exitCode, + runtimeMs + }; + } + + /** + * Get total line count for a process + */ + getOutputLineCount(pid: number): number | null { + const session = this.sessions.get(pid); + if (session) { + return session.outputLines.length; + } + + const completedSession = this.completedSessions.get(pid); + if (completedSession) { + return completedSession.outputLines.length; + } + + return null; + } + + /** + * Legacy method for backward compatibility + * Returns all new output since last read + * @param maxLines Maximum lines to return (default: 1000 for context protection) + * @deprecated Use readOutputPaginated instead + */ + getNewOutput(pid: number, maxLines: number = 1000): string | null { + const result = this.readOutputPaginated(pid, 0, maxLines); + if (!result) return null; + + const output = result.lines.join('\n').trim(); + + // For completed sessions, append completion info with runtime + if (result.isComplete) { + const runtimeStr = result.runtimeMs !== undefined + ? `\nRuntime: ${(result.runtimeMs / 1000).toFixed(2)}s` + : ''; if (output) { - return `${output}\n\nProcess completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s`; + return `${output}\n\nProcess completed with exit code ${result.exitCode}${runtimeStr}`; } else { - return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\n(No output produced)`; + return `Process completed with exit code ${result.exitCode}${runtimeStr}\n(No output produced)`; } } + // Add truncation warning if there's more output + if (result.remaining > 0) { + return `${output}\n\n[Output truncated: ${result.remaining} more lines available. Use read_process_output with offset/length for full output.]`; + } + + return output || null; + } + + /** + * Capture a snapshot of current output state for interaction tracking. + * Used by interactWithProcess to know what output existed before sending input. + */ + captureOutputSnapshot(pid: number): { totalChars: number; lineCount: number } | null { + const session = this.sessions.get(pid); + if (session) { + const fullOutput = session.outputLines.join('\n'); + return { + totalChars: fullOutput.length, + lineCount: session.outputLines.length + }; + } return null; } + /** + * Get output that appeared since a snapshot was taken. + * This handles the case where output is appended to the last line (REPL prompts). + */ + getOutputSinceSnapshot(pid: number, snapshot: { totalChars: number; lineCount: number }): string | null { + const session = this.sessions.get(pid); + if (!session) return null; + + const fullOutput = session.outputLines.join('\n'); + if (fullOutput.length <= snapshot.totalChars) { + return ''; // No new output + } + + return fullOutput.substring(snapshot.totalChars); + } + /** * Get a session by PID * @param pid Process ID diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts index ace21706..93b8384c 100644 --- a/src/tools/improved-process-tools.ts +++ b/src/tools/improved-process-tools.ts @@ -235,8 +235,8 @@ function formatTimingInfo(timing: any): string { } /** - * Read output from a running process (renamed from read_output) - * Includes early detection of process waiting for input + * Read output from a running process with file-like pagination + * Supports offset/length parameters for controlled reading */ export async function readProcessOutput(args: unknown): Promise { const parsed = ReadProcessOutputArgsSchema.safeParse(args); @@ -247,257 +247,127 @@ export async function readProcessOutput(args: unknown): Promise { }; } - const { pid, timeout_ms = 5000, verbose_timing = false } = parsed.data; - - const session = terminalManager.getSession(pid); - if (!session) { - // Check if this is a completed session - const completedOutput = terminalManager.getNewOutput(pid); - if (completedOutput) { - return { - content: [{ - type: "text", - text: completedOutput - }], - }; - } + // Get default line limit from config + const config = await configManager.getConfig(); + const defaultLength = config.fileReadLineLimit ?? 1000; - // Neither active nor completed session found - return { - content: [{ type: "text", text: `No session found for PID ${pid}` }], - isError: true, - }; - } - - let output = ""; - let timeoutReached = false; - let earlyExit = false; - let processState: ProcessState | undefined; + const { + pid, + timeout_ms = 5000, + offset = 0, // 0 = from last read, positive = absolute, negative = tail + length = defaultLength, // Default from config, same as file reading + verbose_timing = false + } = parsed.data; // Timing telemetry const startTime = Date.now(); - let firstOutputTime: number | undefined; - let lastOutputTime: number | undefined; - const outputEvents: any[] = []; - let exitReason: 'early_exit_quick_pattern' | 'early_exit_periodic_check' | 'process_finished' | 'timeout' = 'timeout'; - try { - const outputPromise: Promise = new Promise((resolve) => { - const initialOutput = terminalManager.getNewOutput(pid); - if (initialOutput && initialOutput.length > 0) { - const now = Date.now(); - if (!firstOutputTime) firstOutputTime = now; - lastOutputTime = now; - - if (verbose_timing) { - outputEvents.push({ - timestamp: now, - deltaMs: now - startTime, - source: 'initial_poll', - length: initialOutput.length, - snippet: initialOutput.slice(0, 50).replace(/\n/g, '\\n') - }); - } - - // Immediate check on existing output - const state = analyzeProcessState(initialOutput, pid); - if (state.isWaitingForInput) { - earlyExit = true; - processState = state; - exitReason = 'early_exit_periodic_check'; + // For active sessions with no new output yet, optionally wait for output + const session = terminalManager.getSession(pid); + if (session && offset === 0) { + // Wait for new output to arrive (only for "new output" reads, not absolute/tail) + const waitForOutput = (): Promise => { + return new Promise((resolve) => { + // Check if there's already new output + const currentLines = terminalManager.getOutputLineCount(pid) || 0; + if (currentLines > session.lastReadIndex) { + resolve(); + return; } - resolve(initialOutput); - return; - } - - let resolved = false; - let interval: NodeJS.Timeout | null = null; - let timeout: NodeJS.Timeout | null = null; - - // Quick prompt patterns for immediate detection - const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/; - - const cleanup = () => { - if (interval) clearInterval(interval); - if (timeout) clearTimeout(timeout); - }; - - let resolveOnce = (value: string, isTimeout = false) => { - if (resolved) return; - resolved = true; - cleanup(); - timeoutReached = isTimeout; - if (isTimeout) exitReason = 'timeout'; - resolve(value); - }; - // Monitor for new output with immediate detection - const session = terminalManager.getSession(pid); - if (session && session.process && session.process.stdout && session.process.stderr) { - const immediateDetector = (data: Buffer, source: 'stdout' | 'stderr') => { - const text = data.toString(); - const now = Date.now(); - - if (!firstOutputTime) firstOutputTime = now; - lastOutputTime = now; - - if (verbose_timing) { - outputEvents.push({ - timestamp: now, - deltaMs: now - startTime, - source, - length: text.length, - snippet: text.slice(0, 50).replace(/\n/g, '\\n') - }); - } - - // Immediate check for obvious prompts - if (quickPromptPatterns.test(text)) { - const newOutput = terminalManager.getNewOutput(pid) || text; - const state = analyzeProcessState(output + newOutput, pid); - if (state.isWaitingForInput) { - earlyExit = true; - processState = state; - exitReason = 'early_exit_quick_pattern'; - - if (verbose_timing && outputEvents.length > 0) { - outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern'; - } - - resolveOnce(newOutput); - return; - } - } - }; - - const stdoutDetector = (data: Buffer) => immediateDetector(data, 'stdout'); - const stderrDetector = (data: Buffer) => immediateDetector(data, 'stderr'); - session.process.stdout.on('data', stdoutDetector); - session.process.stderr.on('data', stderrDetector); + let resolved = false; + let interval: NodeJS.Timeout | null = null; + let timeout: NodeJS.Timeout | null = null; - // Cleanup immediate detectors when done - const originalResolveOnce = resolveOnce; - const cleanupDetectors = () => { - if (session.process.stdout) { - session.process.stdout.off('data', stdoutDetector); - } - if (session.process.stderr) { - session.process.stderr.off('data', stderrDetector); - } + const cleanup = () => { + if (interval) clearInterval(interval); + if (timeout) clearTimeout(timeout); }; - // Override resolveOnce to include cleanup - const resolveOnceWithCleanup = (value: string, isTimeout = false) => { - cleanupDetectors(); - originalResolveOnce(value, isTimeout); + const resolveOnce = () => { + if (resolved) return; + resolved = true; + cleanup(); + resolve(); }; - // Replace the local resolveOnce reference - resolveOnce = resolveOnceWithCleanup; - } - - interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - if (newOutput && newOutput.length > 0) { - const now = Date.now(); - if (!firstOutputTime) firstOutputTime = now; - lastOutputTime = now; - - if (verbose_timing) { - outputEvents.push({ - timestamp: now, - deltaMs: now - startTime, - source: 'periodic_poll', - length: newOutput.length, - snippet: newOutput.slice(0, 50).replace(/\n/g, '\\n') - }); - } - - const currentOutput = output + newOutput; - const state = analyzeProcessState(currentOutput, pid); - - // Early exit if process is clearly waiting for input - if (state.isWaitingForInput) { - earlyExit = true; - processState = state; - exitReason = 'early_exit_periodic_check'; - - if (verbose_timing && outputEvents.length > 0) { - outputEvents[outputEvents.length - 1].matchedPattern = 'periodic_check'; - } - - resolveOnce(newOutput); - return; - } - - output = currentOutput; - - // Continue collecting if still running - if (!state.isFinished) { - return; + // Poll for new output + interval = setInterval(() => { + const newLineCount = terminalManager.getOutputLineCount(pid) || 0; + if (newLineCount > session.lastReadIndex) { + resolveOnce(); } + }, 50); - // Process finished - processState = state; - exitReason = 'process_finished'; - resolveOnce(newOutput); - } - }, 50); // Check every 50ms for faster response - - timeout = setTimeout(() => { - const finalOutput = terminalManager.getNewOutput(pid) || ""; - resolveOnce(finalOutput, true); - }, timeout_ms); - }); + // Timeout + timeout = setTimeout(() => { + resolveOnce(); + }, timeout_ms); + }); + }; - const newOutput = await outputPromise; - output += newOutput; - - // Analyze final state if not already done - if (!processState) { - processState = analyzeProcessState(output, pid); - } + await waitForOutput(); + } - } catch (error) { + // Read output with pagination + const result = terminalManager.readOutputPaginated(pid, offset, length); + + if (!result) { return { - content: [{ type: "text", text: `Error reading output: ${error}` }], + content: [{ type: "text", text: `No session found for PID ${pid}` }], isError: true, }; } - // Format response based on what we detected + // Join lines back into string + const output = result.lines.join('\n'); + + // Generate status message similar to file reading let statusMessage = ''; - if (earlyExit && processState?.isWaitingForInput) { - statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`; - } else if (processState?.isFinished) { - statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`; - } else if (timeoutReached) { - statusMessage = '\n⏱️ Timeout reached - process may still be running'; + if (offset < 0) { + // Tail read + statusMessage = `[Reading last ${result.readCount} lines (total: ${result.totalLines} lines)]`; + } else if (offset === 0) { + // "New output" read + if (result.remaining > 0) { + statusMessage = `[Reading ${result.readCount} new lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`; + } else { + statusMessage = `[Reading ${result.readCount} new lines (total: ${result.totalLines} lines)]`; + } + } else { + // Absolute position read + statusMessage = `[Reading ${result.readCount} lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`; + } + + // Add process state info + let processStateMessage = ''; + if (result.isComplete) { + const runtimeStr = result.runtimeMs !== undefined + ? ` (runtime: ${(result.runtimeMs / 1000).toFixed(2)}s)` + : ''; + processStateMessage = `\n✅ Process completed with exit code ${result.exitCode}${runtimeStr}`; + } else if (session) { + // Analyze state for running processes + const fullOutput = session.outputLines.join('\n'); + const processState = analyzeProcessState(fullOutput, pid); + if (processState.isWaitingForInput) { + processStateMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`; + } } // Add timing information if requested let timingMessage = ''; if (verbose_timing) { const endTime = Date.now(); - const timingInfo = { - startTime, - endTime, - totalDurationMs: endTime - startTime, - exitReason, - firstOutputTime, - lastOutputTime, - timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined, - outputEvents: outputEvents.length > 0 ? outputEvents : undefined - }; - timingMessage = formatTimingInfo(timingInfo); + timingMessage = `\n\n📊 Timing: ${endTime - startTime}ms`; } - const responseText = output || 'No new output available'; + const responseText = output || '(No output in requested range)'; return { content: [{ type: "text", - text: `${responseText}${statusMessage}${timingMessage}` + text: `${statusMessage}\n\n${responseText}${processStateMessage}${timingMessage}` }], }; } @@ -526,6 +396,10 @@ export async function interactWithProcess(args: unknown): Promise verbose_timing = false } = parsed.data; + // Get config for output line limit + const config = await configManager.getConfig(); + const maxOutputLines = config.fileReadLineLimit ?? 1000; + // Check if this is a virtual Node session (node:local) if (virtualNodeSessions.has(pid)) { const session = virtualNodeSessions.get(pid)!; @@ -553,6 +427,10 @@ export async function interactWithProcess(args: unknown): Promise inputLength: input.length }); + // Capture output snapshot BEFORE sending input + // This handles REPLs where output is appended to the prompt line + const outputSnapshot = terminalManager.captureOutputSnapshot(pid); + const success = terminalManager.sendInputToProcess(pid, input); if (!success) { @@ -603,6 +481,7 @@ export async function interactWithProcess(args: unknown): Promise const pollIntervalMs = 50; // Poll every 50ms for faster response const maxAttempts = Math.ceil(timeout_ms / pollIntervalMs); let interval: NodeJS.Timeout | null = null; + let lastOutputLength = 0; // Track output length to detect new output let resolveOnce = () => { if (resolved) return; @@ -615,8 +494,12 @@ export async function interactWithProcess(args: unknown): Promise interval = setInterval(() => { if (resolved) return; - const newOutput = terminalManager.getNewOutput(pid); - if (newOutput && newOutput.length > 0) { + // Use snapshot-based reading to handle REPL prompt line appending + const newOutput = outputSnapshot + ? terminalManager.getOutputSinceSnapshot(pid, outputSnapshot) + : terminalManager.getNewOutput(pid); + + if (newOutput && newOutput.length > lastOutputLength) { const now = Date.now(); if (!firstOutputTime) firstOutputTime = now; lastOutputTime = now; @@ -626,12 +509,13 @@ export async function interactWithProcess(args: unknown): Promise timestamp: now, deltaMs: now - startTime, source: 'periodic_poll', - length: newOutput.length, - snippet: newOutput.slice(0, 50).replace(/\n/g, '\\n') + length: newOutput.length - lastOutputLength, + snippet: newOutput.slice(lastOutputLength, lastOutputLength + 50).replace(/\n/g, '\\n') }); } - output += newOutput; + output = newOutput; // Replace with full output since snapshot + lastOutputLength = newOutput.length; // Analyze current state processState = analyzeProcessState(output, pid); @@ -669,9 +553,19 @@ export async function interactWithProcess(args: unknown): Promise await waitForResponse(); // Clean and format output - const cleanOutput = cleanProcessOutput(output, input); + let cleanOutput = cleanProcessOutput(output, input); const timeoutReached = !earlyExit && !processState?.isFinished && !processState?.isWaitingForInput; + // Apply output line limit to prevent context overflow + let truncationMessage = ''; + const outputLines = cleanOutput.split('\n'); + if (outputLines.length > maxOutputLines) { + const truncatedLines = outputLines.slice(0, maxOutputLines); + cleanOutput = truncatedLines.join('\n'); + const remainingLines = outputLines.length - maxOutputLines; + truncationMessage = `\n\n⚠️ Output truncated: showing ${maxOutputLines} of ${outputLines.length} lines (${remainingLines} hidden). Use read_process_output with offset/length for full output.`; + } + // Determine final state if (!processState) { processState = analyzeProcessState(output, pid); @@ -725,6 +619,10 @@ export async function interactWithProcess(args: unknown): Promise responseText += `\n\n${statusMessage}`; } + if (truncationMessage) { + responseText += truncationMessage; + } + if (timingMessage) { responseText += timingMessage; } diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 0406ba86..906a9fec 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -28,6 +28,8 @@ export const StartProcessArgsSchema = z.object({ export const ReadProcessOutputArgsSchema = z.object({ pid: z.number(), timeout_ms: z.number().optional(), + offset: z.number().optional(), // Line offset: 0=from last read, positive=absolute, negative=tail + length: z.number().optional(), // Max lines to return (default from config.fileReadLineLimit) verbose_timing: z.boolean().optional(), }); diff --git a/src/types.ts b/src/types.ts index fb2f7d94..1d030f86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,8 @@ export interface ProcessInfo { export interface TerminalSession { pid: number; process: ChildProcess; - lastOutput: string; + outputLines: string[]; // Line-based buffer (persistent) + lastReadIndex: number; // Track where "new" output starts for default reads isBlocked: boolean; startTime: Date; } diff --git a/test/test-process-pagination.js b/test/test-process-pagination.js new file mode 100644 index 00000000..30da2c02 --- /dev/null +++ b/test/test-process-pagination.js @@ -0,0 +1,268 @@ +import assert from 'assert'; +import { startProcess, readProcessOutput, interactWithProcess } from '../dist/tools/improved-process-tools.js'; + +/** + * Test suite for process output pagination features + * Tests offset/length parameters and context overflow protection + */ + +// Helper to extract PID from start result +function extractPid(result) { + const match = result.content[0].text.match(/PID (\d+)/); + return match ? parseInt(match[1]) : null; +} + +// Helper to wait +const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Test 1: Basic offset=0 (new output) behavior for RUNNING processes + */ +async function testNewOutputBehavior() { + console.log('\n📋 Test 1: Basic new output behavior (offset=0) for running process...'); + + // Start a long-running process that outputs incrementally + const startResult = await startProcess({ + command: 'node -e "let i=0; setInterval(() => { console.log(\'tick\' + i++); if(i>5) process.exit(0); }, 200)"', + timeout_ms: 500 // Return before completion + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + // First read - get initial output + const read1 = await readProcessOutput({ pid, timeout_ms: 300 }); + assert(!read1.isError, 'First read should succeed'); + const lines1 = (read1.content[0].text.match(/tick\d/g) || []).length; + console.log(` First read got ${lines1} tick lines`); + + // Wait for more output + await wait(400); + + // Second read should get NEW output only + const read2 = await readProcessOutput({ pid, timeout_ms: 300 }); + assert(!read2.isError, 'Second read should succeed'); + const text2 = read2.content[0].text; + + // Should NOT re-read tick0 if we already read it + // (unless process completed, in which case all output is available) + if (text2.includes('Process completed')) { + console.log(' Process completed - all output available'); + } else { + console.log(` Second read status: ${text2.split('\n')[0]}`); + } + + console.log('✅ Test 1 passed: New output behavior works correctly'); +} + +/** + * Test 2: Positive offset (absolute position) + */ +async function testAbsoluteOffset() { + console.log('\n📋 Test 2: Absolute position (positive offset)...'); + + const startResult = await startProcess({ + command: "node -e \"for(let i=0; i<10; i++) console.log('line' + i)\"", + timeout_ms: 3000 + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + await wait(500); + + // Read from line 5 + const read = await readProcessOutput({ pid, offset: 5, length: 3, timeout_ms: 1000 }); + assert(!read.isError, 'Read should succeed'); + assert(read.content[0].text.includes('line5'), 'Should contain line5'); + assert(read.content[0].text.includes('line6'), 'Should contain line6'); + assert(read.content[0].text.includes('line7'), 'Should contain line7'); + assert(!read.content[0].text.includes('line4'), 'Should NOT contain line4'); + assert(read.content[0].text.includes('from line 5'), 'Status should show reading from line 5'); + + console.log('✅ Test 2 passed: Absolute position works correctly'); +} + +/** + * Test 3: Negative offset (tail behavior) + */ +async function testTailBehavior() { + console.log('\n📋 Test 3: Tail behavior (negative offset)...'); + + const startResult = await startProcess({ + command: "node -e \"for(let i=0; i<20; i++) console.log('line' + i)\"", + timeout_ms: 3000 + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + await wait(500); + + // Read last 5 lines (output has 21 lines: line0-line19 + empty) + // Last 5 lines should include line16, line17, line18, line19 + const read = await readProcessOutput({ pid, offset: -5, timeout_ms: 1000 }); + assert(!read.isError, 'Read should succeed'); + assert(read.content[0].text.includes('line16'), 'Should contain line16'); + assert(read.content[0].text.includes('line19'), 'Should contain line19'); + assert(!read.content[0].text.includes('line15'), 'Should NOT contain line15'); + assert(read.content[0].text.includes('Reading last'), 'Status should indicate tail read'); + + console.log('✅ Test 3 passed: Tail behavior works correctly'); +} + +/** + * Test 4: Length limit enforcement + */ +async function testLengthLimit() { + console.log('\n📋 Test 4: Length limit enforcement...'); + + const startResult = await startProcess({ + command: "node -e \"for(let i=0; i<100; i++) console.log('line' + i)\"", + timeout_ms: 3000 + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + await wait(500); + + // Read with length limit of 10 from absolute position 0 + const read = await readProcessOutput({ pid, offset: 1, length: 10, timeout_ms: 1000 }); + assert(!read.isError, 'Read should succeed'); + + const outputText = read.content[0].text; + + // Should show "remaining" since we're only reading 10 of 100 lines + assert(outputText.includes('remaining'), 'Should show remaining lines'); + assert(outputText.includes('Reading 10 lines'), 'Should indicate reading 10 lines'); + + console.log('✅ Test 4 passed: Length limit works correctly'); +} + +/** + * Test 5: Runtime info for completed processes + */ +async function testRuntimeInfo() { + console.log('\n📋 Test 5: Runtime info for completed processes...'); + + const startResult = await startProcess({ + command: "node -e \"setTimeout(() => console.log('done'), 500)\"", + timeout_ms: 200 // Return before completion + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + // Wait for process to complete + await wait(1000); + + const read = await readProcessOutput({ pid, timeout_ms: 1000 }); + assert(!read.isError, 'Read should succeed'); + assert(read.content[0].text.includes('runtime:'), 'Should show runtime'); + assert(read.content[0].text.includes('Process completed'), 'Should show completion'); + + console.log('✅ Test 5 passed: Runtime info works correctly'); +} + +/** + * Test 6: interact_with_process output truncation + */ +async function testInteractTruncation() { + console.log('\n📋 Test 6: interact_with_process output truncation...'); + + // Start a Python REPL + const startResult = await startProcess({ + command: 'python3 -i', + timeout_ms: 3000 + }); + + const pid = extractPid(startResult); + if (!pid) { + console.log('⚠️ Test 6 skipped: Could not start Python REPL'); + return; + } + + await wait(500); + + // Generate lots of output (more than default 1000 lines) + const result = await interactWithProcess({ + pid, + input: 'for i in range(1500): print(f"line {i}")', + timeout_ms: 10000 + }); + + if (result.isError) { + console.log('⚠️ Test 6 skipped: Python interaction failed'); + return; + } + + const outputText = result.content[0].text; + + // Check for truncation warning + if (outputText.includes('truncated')) { + assert(outputText.includes('use read_process_output'), 'Should suggest using read_process_output'); + console.log('✅ Test 6 passed: Output truncation warning works'); + } else { + // If fileReadLineLimit is set higher than 1500, no truncation expected + console.log('✅ Test 6 passed: Output within limits (no truncation needed)'); + } +} + +/** + * Test 7: Re-reading output with absolute offset + */ +async function testReReadOutput() { + console.log('\n📋 Test 7: Re-reading output with absolute offset...'); + + const startResult = await startProcess({ + command: "node -e \"for(let i=0; i<5; i++) console.log('data' + i)\"", + timeout_ms: 3000 + }); + + const pid = extractPid(startResult); + assert(pid, 'Should get PID'); + + await wait(500); + + // First read with offset=0 (consumes the "new" pointer for running sessions) + const read1 = await readProcessOutput({ pid, offset: 0, timeout_ms: 1000 }); + assert(!read1.isError, 'First read should succeed'); + + // Re-read from beginning using absolute offset + const read2 = await readProcessOutput({ pid, offset: 1, length: 3, timeout_ms: 1000 }); + assert(!read2.isError, 'Second read should succeed'); + assert(read2.content[0].text.includes('data1'), 'Should re-read data1'); + assert(read2.content[0].text.includes('data2'), 'Should re-read data2'); + + console.log('✅ Test 7 passed: Re-reading with absolute offset works'); +} + +// Run all tests +async function runAllTests() { + console.log('🚀 Starting process pagination tests...\n'); + + try { + await testNewOutputBehavior(); + await testAbsoluteOffset(); + await testTailBehavior(); + await testLengthLimit(); + await testRuntimeInfo(); + await testInteractTruncation(); + await testReReadOutput(); + + console.log('\n🎉 All pagination tests passed!'); + return true; + } catch (error) { + console.error('\n❌ Test failed:', error.message); + console.error(error.stack); + return false; + } +} + +runAllTests() + .then(success => process.exit(success ? 0 : 1)) + .catch(error => { + console.error('Test error:', error); + process.exit(1); + }); From 9ff639c8325603271519dbf060c41a5a79c9edbe Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 10 Dec 2025 15:48:24 +0200 Subject: [PATCH 2/5] fix: length parameter now caps tail reads in process output pagination - Fixed bug where negative offset (tail) reads ignored the length parameter - offset=-50, length=10 now correctly returns only 10 lines (not 50) - Updated documentation to reflect correct behavior - Prevents context overflow when using tail with large offsets --- src/server.ts | 2 +- src/terminal-manager.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index b48b63cf..3ef6b7e1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -794,7 +794,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { - offset: 0 → All new lines (respects config limit) - offset: 500, length: 50 → Lines 500-549 (absolute position) - offset: -20 → Last 20 lines (tail) - - offset: -100, length: 50 → Last 100 lines (length ignored for tail) + - offset: -100, length: 50 → Last 50 lines (length caps tail) OUTPUT PROTECTION: - Uses same fileReadLineLimit as read_file (default: 1000 lines) diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index ed981795..65cf4577 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -451,9 +451,11 @@ export class TerminalManager { if (offset < 0) { // Negative offset = tail behavior (read last N lines) - const tailCount = Math.abs(offset); + // Cap at length to prevent context overflow + const requestedTail = Math.abs(offset); + const tailCount = Math.min(requestedTail, length); startIndex = Math.max(0, totalLines - tailCount); - linesToRead = lines.slice(startIndex); + linesToRead = lines.slice(startIndex, startIndex + tailCount); // Don't update lastReadIndex for tail reads } else if (offset === 0) { // offset=0 means "from where I last read" (like getNewOutput) From 3355b686d449c286b1c2102b4000dbe461f0ff4e Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 10 Dec 2025 15:55:42 +0200 Subject: [PATCH 3/5] fix: tail reads now properly respect length parameter and show accurate status - Fixed bug where negative offset (tail) reads ignored the length parameter - offset=-50, length=10 now correctly returns 10 lines starting 50 from end - Updated status message to show actual line range instead of misleading 'last N lines' - Example: 'Reading lines 51-60 (10 lines, starting 50 from end)' vs 'Reading last 10 lines' - Prevents context overflow when using tail with large offsets --- docs/privacy-policy-update-meeting.md | 150 ++ src/server.ts | 2 +- src/terminal-manager.ts | 11 +- src/tools/improved-process-tools.ts | 5 +- test/samples/01_sample_simple.pdf.md | 13 + test/samples/02_sample_invoice.pdf.md | 37 + .../page_undefined_img_1.webp | Bin 0 -> 4390 bytes .../page_undefined_img_2.webp | Bin 0 -> 736 bytes test/samples/03_sample_compex.pdf.md | 2142 +++++++++++++++++ test/samples/URL Sample.md | 7 + 10 files changed, 2358 insertions(+), 9 deletions(-) create mode 100644 docs/privacy-policy-update-meeting.md create mode 100644 test/samples/01_sample_simple.pdf.md create mode 100644 test/samples/02_sample_invoice.pdf.md create mode 100644 test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp create mode 100644 test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp create mode 100644 test/samples/03_sample_compex.pdf.md create mode 100644 test/samples/URL Sample.md diff --git a/docs/privacy-policy-update-meeting.md b/docs/privacy-policy-update-meeting.md new file mode 100644 index 00000000..04bfc282 --- /dev/null +++ b/docs/privacy-policy-update-meeting.md @@ -0,0 +1,150 @@ +# Privacy Policy Update - Team Discussion + +**Date:** December 8, 2025 +**PR:** https://github.com/wonderwhy-er/DesktopCommanderMCP/pull/287 +**Duration:** 15-20 minutes + +--- + +## Context: Why This Update? + +1. **Code audit revealed inconsistencies** between what the privacy policy said and what the code actually does +2. **Australian Privacy Act complaint** highlighted specific issues (APP 1, 2, 3, 5) +3. **No changes to actual data collection** — this is about making the policy accurate, not changing behavior + +--- + +## Summary of Changes + +### 1. "Anonymous" → "Pseudonymous" + +**Before:** "Anonymous client ID... is not connected to any other telemetry event data" + +**After:** "Pseudonymous client ID... is included with telemetry events" + +**Why:** The UUID is sent with every event (we need this for retention metrics). Calling it "anonymous" and "isolated" was factually incorrect. Under GDPR, pseudonymous data is still personal data. + +--- + +### 2. Removed "No PII" Claim + +**Before:** "avoiding any personally identifiable information (PII)" + +**After:** Lists specifically what we don't collect (names, emails, usernames, file paths) + +**Why:** +- Under US law: UUID might not be PII +- Under GDPR: UUID IS personal data +- Safer to be specific about what we don't collect rather than make broad claims + +--- + +### 3. Added Missing Data Fields + +**Added disclosures for:** +- Client info (Claude Desktop, VS Code version) +- Container/Docker metadata +- File sizes +- Runtime source + +**Why:** These were being collected but not documented. Full transparency. + +--- + +### 4. Named Google Analytics Explicitly + +**Before:** "sent securely via HTTPS to Google Analytics" (buried in text) + +**After:** Dedicated "Analytics Provider" section naming GA4 + +**Why:** Industry standard for developer tools. Users expect to know who processes their data. + +--- + +### 5. IP Address Clarification + +**Added:** "We do not store IP addresses. However, Google Analytics receives them via HTTPS and auto-anonymizes them." + +**Why:** We can't claim "we don't collect IPs" when our analytics provider sees them. This is honest about the technical reality. + +--- + +### 6. Added "Your Rights" Section + +**New section includes:** +- List of GDPR rights (access, deletion, objection, withdraw consent) +- How to find your UUID (ask AI or check config file) +- Explanation that we can only process requests with UUID +- 30-day response commitment + +**Why:** +- GDPR/Australian Privacy Act require this +- Explains the privacy paradox: we're SO private we can't identify users +- This is actually a strength, not a weakness + +--- + +### 7. Added Legal Contact Email + +**Added:** legal@desktopcommander.app for privacy/legal matters + +**Why:** +- GitHub issues are public — not appropriate for legal matters +- Required for compliance +- We already have this email, just wasn't in the policy + +--- + +### 8. Simplified README Section + +**Before:** ~25 lines of privacy details in README + +**After:** 5 lines + link to PRIVACY.md + +**Why:** Single source of truth, easier maintenance, README was already 960+ lines + +--- + +## Discussion Points + +### A. Are we comfortable with "pseudonymous" language? +- Legally accurate +- Might sound scarier than "anonymous" to some users +- But: honesty > marketing + +### B. The Australian complaint +- Changes address APP 1 (clear policy), APP 5 (proper notice) +- APP 2 (anonymity option) — we can argue UUID-based system IS effectively anonymous since we can't re-identify +- APP 3 (necessity) — retention metrics are legitimate business need + +### C. Should we add anything else? +- Children's data statement? ("Not directed at under-18s") +- Cross-border transfer note? (Data processed in US) +- More detailed retention explanation? + +--- + +## Decisions Needed + +1. ✅ / ❌ Approve PR as-is? +2. ✅ / ❌ Add children's data statement? +3. ✅ / ❌ Add cross-border transfer note? +4. ✅ / ❌ Update website privacy policy to match? + +--- + +## Files Changed + +| File | Changes | +|------|---------| +| `PRIVACY.md` | Major rewrite — all changes above | +| `README.md` | Simplified privacy section, links to PRIVACY.md | + +--- + +## Post-Meeting Actions + +- [ ] Merge PR +- [ ] Update https://legal.desktopcommander.app/privacy_desktop_commander_mcp +- [ ] Mention in next release notes +- [ ] Respond to Discord complaint with link to updated policy diff --git a/src/server.ts b/src/server.ts index 3ef6b7e1..a3539211 100644 --- a/src/server.ts +++ b/src/server.ts @@ -794,7 +794,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { - offset: 0 → All new lines (respects config limit) - offset: 500, length: 50 → Lines 500-549 (absolute position) - offset: -20 → Last 20 lines (tail) - - offset: -100, length: 50 → Last 50 lines (length caps tail) + - offset: -50, length: 10 → Start 50 from end, read 10 lines OUTPUT PROTECTION: - Uses same fileReadLineLimit as read_file (default: 1000 lines) diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index 65cf4577..98005c53 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -450,12 +450,11 @@ export class TerminalManager { let linesToRead: string[]; if (offset < 0) { - // Negative offset = tail behavior (read last N lines) - // Cap at length to prevent context overflow - const requestedTail = Math.abs(offset); - const tailCount = Math.min(requestedTail, length); - startIndex = Math.max(0, totalLines - tailCount); - linesToRead = lines.slice(startIndex, startIndex + tailCount); + // Negative offset = start position from end, then read 'length' lines forward + // e.g., offset=-50, length=10 means: start 50 lines from end, read 10 lines + const fromEnd = Math.abs(offset); + startIndex = Math.max(0, totalLines - fromEnd); + linesToRead = lines.slice(startIndex, startIndex + length); // Don't update lastReadIndex for tail reads } else if (offset === 0) { // offset=0 means "from where I last read" (like getNewOutput) diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts index 93b8384c..8c7a42e1 100644 --- a/src/tools/improved-process-tools.ts +++ b/src/tools/improved-process-tools.ts @@ -325,8 +325,9 @@ export async function readProcessOutput(args: unknown): Promise { // Generate status message similar to file reading let statusMessage = ''; if (offset < 0) { - // Tail read - statusMessage = `[Reading last ${result.readCount} lines (total: ${result.totalLines} lines)]`; + // Tail read - show actual line range + const endLine = result.readFrom + result.readCount - 1; + statusMessage = `[Reading lines ${result.readFrom}-${endLine} (${result.readCount} lines, starting ${Math.abs(offset)} from end, total: ${result.totalLines} lines)]`; } else if (offset === 0) { // "New output" read if (result.remaining > 0) { diff --git a/test/samples/01_sample_simple.pdf.md b/test/samples/01_sample_simple.pdf.md new file mode 100644 index 00000000..83532e5c --- /dev/null +++ b/test/samples/01_sample_simple.pdf.md @@ -0,0 +1,13 @@ +# Hello World + +This is a test PDF created from markdown. + +## Features + + Simple text Bold text Italic text Link + +## Code + + console.log('Hello World'); + + diff --git a/test/samples/02_sample_invoice.pdf.md b/test/samples/02_sample_invoice.pdf.md new file mode 100644 index 00000000..da79f375 --- /dev/null +++ b/test/samples/02_sample_invoice.pdf.md @@ -0,0 +1,37 @@ +## POWERED BY + +#### 1 + + Sub Total 20,000.00 Total Rs.20,000.00 + +### Balance Due Rs.20,000.00 + + Authorized Signature + + Total In Words Indian Rupee Twenty Thousand Only + + Notes + + Thanks for your business. + + Payment Options + + Terms & Conditions GST not applicable + +## Tattva Trails Private Limited + + Some street 12 Panjim Goa 999888 India 12344321 tattva.trails.official@gmail.com + +# TAX INVOICE + +#### # : INV-000007 + + Invoice Date : 20/11/2025 Terms : Due on Receipt Due Date : 20/11/2025 + + Bill To + +### Shanti Spa Ayurveda + + # Item & Description Qty Rate Amount 1 Referral Fee – Panchakarma Treatment Package (14 Days) 1.00 20,000.00 20,000.00 + + diff --git a/test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp b/test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp new file mode 100644 index 0000000000000000000000000000000000000000..8bd1acd3f0ea5618a970f79dd2cd75f264d5de43 GIT binary patch literal 4390 zcmV+>5!voiNk&E<5dZ*JMM6+kP&il$0000G0002T002q=06|PpNI(Vv00HoaZQJok z+V&kusjKaA+qT_3_Oxx=wr$(CZQHh0=2UWSe_ToGy!y^nx0fX%CP4mAb>fB#_ih_k zA>mi=EiSJ#_D|RiQn-KBFHHjOPld-3vvMSuQk7{wVcUrvLvy+EeGG7G`id|Y;M`Wp zywt_ME0NgBKCj$v#=%z#*Um-B40Qlz+C`Fee&_YknFb;*~RD&3&&cOjJr?=Eo69yhoCa#Q($bxm~``U=tCk7!54V>HwxnJd<~Q>_YR0 z^J_=7sGPySzyXEXx4!Wfz6bDBz1TmkO~v%$@l@dcbik1%J|aG~PRGyj|(Hoa>_qUI0S~7)b{0ne|0~L)Tecc}njPeeN zJ_0dA3j~PvTz~>7t4F?$bGKG>+Ft@nsidOW_Iof89N-mx1ZW^;`o^LRXGC)w!1>g` z&pHZWfC9Qi>Ht*q3cY}*V(xVmKy_WyfaCVY(ZE3+e*sFtU;6xFfu>$zAaKc~waReM zu4^{1L!>Vd7pS2&u6yZ|Frc+pXbyzg%o(Bp+KcXiz)g`n0NG5KV*zCVzA>k^;bRhQ z^?)dw6g6ZvVZQ~WQowiSR5Xkh?WF*|k%>U0uL+MVKwbk~%vk`a>iW_dsw58qHi`*x zeim>Dm}^S2L3Q*Y$vRS|k;Fu{x0i{-%a2c5L7`P<5VioYc1+)j=n~~_9 zfy*Xr073v7XbXM=w6lOJ0O?FH4GN%- z=vxkKwg3qN+UWZM1MsuHYOW$unJ@&nW(6mKsrr@zA6@}H^<_pGW{3%mfCwvC0PNFu z9hkTWSftPAZv{BAnNb$ta|`GWJlFROXgwJ?txxV5wul*@8_HV1Pk>6U?J_W@6%b;= zUVsXwNCeQ(0x|)V)HWN4=?n0cw!rtmBQfV1FvJ3E5kM1dr4`^bRMXZ5AjqW6z;X+a zi@;cIvL3e`b=mF!N~JMp0&vO-Rsh@erM&9qUSoefzhIz*IqiTqR?rW4pife+Q7+(B zzce}y0ZLKFoL>Q_75oV}66q5uw%rv(%*qk3!pZ>s&1nmSTR|>>f@Wkq;|`qLs@XhS z1}FuKOi6whIAI08F+e>%DN8gMzV2!qz@_UhfLk4sdypX(Z~{loh`9sM-U{RzFfl$~ zR_!qB@Ebe~$|6;FI$c2M7T%C@usMO3xtq+Fa3u{NUsGPwr@ys*-r3o+R=NIp5 zPvIw@wqFXnc%Cx3=ueVHcM|ze^1u`+Ql#+cn+zWLCl#@$@F;PP6bY8SK-`z^3SEc0 zr97AzD)JY3+}TD(;aC=!KuMdiQtW7Po8;y3Z!(^IpWNN#aFOcdb=O`sN%}|Zlmv>{ z4&MxL_Z#1fRHM2F(Ty8V_vFk)5!v)6q0%|7q_}TLagjna^T2Wa`cqGC?-P-E(Fv3s zkz>TeylRL@Z&kp9aFObstO!Xb{`Zb2OzfxB`sK?4+{iq~x)p!V6>T{`JeqkiqD;ah zaMbA0qeuC>v(=nCr@DCf!$6VTqtm*RuZAognN9v5HC9kKAh-$u0MIu8odGJy080Qq zQ6!ETnU>*MubZyxYJ+xr0g z(f!SI6AN4mf6{S!R~{=MK0^xyTqz#qoHvwx=hyZCDVZ~RB)=d|C-zq3h=ermS-Gg9F@t*U}t? zj$U7bmV>Um(lxrsEB6zMX`)-Rq}pfPTAA-q0RH{JjQ=A6Sx0{e1jk#}$4rSvromsN z`C;R=UL^`QWM6cx6@6cPxRkETcz}@V*Eq!m1DBLkyN8SuX>Lc~tMs z6m~~|ifEe&4%WIWDnU6K?E^1-*M)wUDiL!uz=vWxS(y(7BD~o55-2qWEgU(y7O!h4=CJrBEzWb<1_yjg9|X0ILoTo_bXf3d>Ly*NzC;jmqrl)HJw{Pq(d z?OQoO*YsX^7$>p4hpwJ*z1&==d6tN?SQXybF0$Md4E`9iIfXD$eUh+2*$nB*@>viq zx*;*vDW&#Za2uPb!*T69u1)I_Q+X-%V7TEl5TiXZs78$uW4<$fWS z2-0+3sv*vOvv4V_Fn!r{&NV8jp%4aiG>jX(^wj4h4QY9xM4Ggw#!9*0g^{JJ(sTVY zUR%6`MV*^IV*ga1$B!jwP3~E#9p><%SCW4(zW*Ht9Jy8_-=jXrw0}<18*(R;Vdvwswa7!30=}i*Lso**y<~AhlS5t*9LRKjN z4=X-5?!3Li)&3spR5DL7SG1;NEB2m|ERgQN{Z9`i{&JilU)yh~qOo|Fr+%2p@1cCT z#fZf{WNPl&pbYOU9+0ax;+K5T%vLzYb4VV0G(YjH9gN4@K2wkVV7rjHmr8X9bBff` zNj$PZbxVKQvAG%2UAhkfO4dGs5B;EfPx#I6Ykdb`zrxsfkzW}<8(mR()r(}jfYLnY zY-vi4@^?7r>|s0r+Tx%DuPLlMamgZ!^$5gjAyer?+Mh9u2CLm68~wUs5;_2XaQQ1 zb+B|uT`LyN()Q=~_voyK3V-O;bR}zqyWA*V^K$U9A}5kA%pCF_@212cb0*f!b=`*a z%8U{JsnX+1eF-l;J}JkrScRh2sD%qF3D+_+I>XHE^X)#T4`VBC-H!N}cBR7_c_f$; z@fRog?ex%}t6z7>VR8A{{VCsFNRmZ|_+FQaTDrsn@p#Y93Pc^kg8kL`)-?@Lx&o@t zaLw)OgS8&zm-$Z;qvQX}5)pxR<(3j@Urd}G&0z)aO^S`@3j4{Ywn)e&OT=ehJGYT zv(-ytUE4EPbQE4i*)SDwT(_XTB(<;yGaIZl1l;0`XLY=`08qWUMLc-UL0|XKMoG1m zSOWwqENaSJgPC)bobuttE~!5_Fxl#TfE!H8lJzusaz|Ja9d>~eQ4~76t4!j^R%L+F z@KZSh8RwH8X;uB^|hI*Lvl&eF59H+#`vA)O&)16d<7>u>1%z&6z%l+8EqMD*w$(0NN>dtrtR$K3WcR`ANH0bi~3Nurr*JXKBMvg$oF%3rBH}qbQ^nR znoL#h^}(+-3@w9E;&#yTI%cas-eLLi*D3$Wx5=IQ`E>zg0&xi-Bw=e>Iz$^N8|6oa#CE*zg_oA;XLHltwtmzo4GLR;vRUVJ;U%cEI5C?1whIQ zUQBEu*fx|^HZw0cGZHc0uiq*k7JoSZcmZe}3C-41Fl` z3W(t7u?`Qe;2C`>5F1YDsG(SCftjvYk;oWCKXfWcI5GER@I^`H4+3i0 zx-*Z`Yr80!MJo-o3K)aF?6Rr(G}~R;f8VmT8rZ5!B(a;L>pR(&dUoA@NKa&Ny0h1v zB>u=?HV6@-F4NA6tq+;_MQ2e@;-B=QQu3{nbV%UGY6ilL2ULLvz3B8-YWj**V|X;V z4f%>8+JB+d@of#i;mT3}B-~rzvOfdq&3-a(XJI5-6pUX2GG4MGMPsdk_zyJUM0MjI{&;S4c literal 0 HcmV?d00001 diff --git a/test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp b/test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp new file mode 100644 index 0000000000000000000000000000000000000000..c0352072c6c2627dae4f5ae199ffe13a11980af2 GIT binary patch literal 736 zcmV<60w4WSNk&H40ssJ4MM6+kP&il$0000G0000J0RS%m06|PpNLl~@00Cd1rfnlQ z*i4;^;E25u5h~1Mnt)Ah?M-MdkkI`gp^M0rDSJ$?!tXHe8z~X}pMWe~cNx=HncUH^ zwVs1B=?R>u=j7P$zsIV;iFyvstY_pjdNOxIe3cGXP&go10ssI|5dfV5Dii@Q06tM7 zkVYe+p`jxaI`D7`326Wo#ItVyb_Rg*!~kl31d9KH8DTJ?H<(ZT%G*7KE%?@dXrU2GZ!;>J;B4>Eo^n@{|L z`J%rT-OLG~4oHC}9_+CK63Dx6v3lwb*#M_$`hWob{dRdF`w7cYp+(2;m}%C4x1~sF zn~zIO+8>@9%DzLdaS~&VHL>|hs(<_EORRDb4VN-NUheHYkUfkXv%q6l*L53rRVLXH zA7mf4G&hx%31HfRJLF1}7b>v(2_SFxqJ!Pe@KU|^({jt5-*h`3^PoKdz@VU>dviXx z-^JVgnSxSZe{)+Iu!8=mOj4~B@AF*kD(`wD_$^)jJAd^xrC-*|h7M5%mbQQ5m0*?A>ppnyE0I3GS?jfI2oNb_NIM(R?fkN7hUa8^ z<^5hXZU1EnTWeFKX`5~CpP%aN*-vZE5veOzDkb%C|I5kf4q<+3ZnB{ks`IEd5K2FT zpSjxtV}Gkw4!tEFg2bW)MQ(Ti4l;TDNKsNX7cwz!!>{pGzHGc}1B_~G0CgDR39UY| z_ddaL5}tE`hGAi}V4nl~y;87yYI`D*i+W*2OVT#Uqx&^8l{uw_CM{J@C8*Jc|Nk#c zIGtTi_s{nr!VoM;))0TEgukjQ-SjzVq6_|e#dewW)X)g$*bJ9PChzl~>+^9~5jAW8 SzEUY+Mn}Vap2b*HKmY(a+ghan literal 0 HcmV?d00001 diff --git a/test/samples/03_sample_compex.pdf.md b/test/samples/03_sample_compex.pdf.md new file mode 100644 index 00000000..927b233a --- /dev/null +++ b/test/samples/03_sample_compex.pdf.md @@ -0,0 +1,2142 @@ +# arXiv:2302.11942v3 [q-fin.MF] 10 Mar 2023 + +## Liquidity Providers Greeks and Impermanent Gain + +### Niccolò Bardoscia, Alessandro Nodari ∗ + +### 23 February 2023 + +## Abstract + + In traditional finance, the Black & Scholes model has guided almost 50 years of derivatives pricing, defining a standard to model any volatility-based product. With the rise of Decentralized Finance (DeFi) and constant product Automated Market Makers (AMMs), Liquidity Providers (LPs) are playing an increasingly important role in markets functioning, but, as the recent bear market highlighted, they are exposed to important risks such as Impermanent Loss (IL). In this paper, we tailor the formulas introduced by Black & Scholes to DeFi, proposing a method to calculate the greeks of an LP. We also introduce Impermanent Gain, a product that LPs can use to hedge their position and traders can use to bet on a rise in volatility and benefit from large market moves. + + Keywords: liquidity providers, impermanent loss, constant product automated market makers, impermanent gain + + ∗The authors are greteful to 0xSami_, Andrea Bugin, Andrea Prampolini, Barry Fried, Carlo Sala, Fabio Bellini, Giulio Anselmi and Miguel Ottina for providing valuable feedback on drafts of the article. + + +## 1 Introduction + +Before diving deeper on the subjects of this article we will present, in this first section, a brief review of the ecosystem in which we will operate and of its principal actors. + +### 1.1 Decentralized Exchanges + +A Decentralized Exchange (DEX) is a peer-to-peer marketplace where transactions occur directly between crypto traders. DEXs fulfill one of crypto’s core possibilities: fostering financial transactions that aren’t officiated by banks, brokers, payment processors, or any other kind of intermediary. The most popular DEXs, like Uniswap and Sushiswap, utilize the Ethereum blockchain and are part of the growing suite of DeFi tools which make a huge range of financial services available directly from a compatible crypto wallet. Unlike Centralized Exchanges (CEXs) like Binance and Coinbase, DEXs don’t allow for exchanges between fiat and crypto. All the transactions in a CEX are handled by the exchange itself via an order book that establishes the price for a particular cryptocurrency based on current buy and sell orders. DEXs, on the other hand, are simply a set of smart contracts: they establish the prices of various cryptocurrencies against each other algorithmically and use Liquidity Pools, in which investors lock funds in exchange for a percentage of the trading fees, in order to facilitate trades. While transactions on a CEX are recorded on that exchange’s internal database, DEX transactions are settled directly on the blockchain. DEXs are usually built on open-source code and developers can adapt existing code to create new competing projects. + +### 1.2 Liquidity Provider + +A Liquidity Provider in crypto, from here on LP, is an investor (individual or institution) who, as the name suggests, funds a liquidity pool with crypto assets it owns in order to facilitate trading on the platform and earn passive income on its deposit. The more assets in a pool, the more liquidity the pool has, and the easier trading becomes on a DEX for other market participants hence the crucial role played by LPs. How much the DEX pays the LP is based on the percentage of the crypto liquidity pool it puts, the volume, and the transaction fee offered by the exchange to LPs. + +1.2.1 Liquidity Provider Tokens + +Liquidity Provider tokens (LP tokens) are crypto tokens given to users who deposit their crypto into a Liquidity Pool. LP tokens represent the Liquidity Provider’s share of the pool and can be redeemed at any time for the underlying assets. Some platforms require LP tokens to be locked for a period of time in order to access additional rewards (Liquidity Mining). + +### 1.3 Constant product AMM + +Here we will present in brief what is a constant product AMM for a more in depht analysis of AMM see for example [1], [2], [3] and [4]. A constant product AMM is a type of AMM in which the reserves of tokens are regulated by a product function, given two tokens x and y we have that: + + x · y = k + +Where x and y represent, with an abuse of notation, the quantities of token x and y respectively and k is the constant. For example in Uniswap, see [5] and [6], they call that constant L^2 so that: + + √ x · y = L + +We will indicate with St the pool price of token x in terms of token y at time t. In this type of AMM we can derive the price as the ratio of the number of token y and of token x at time t: + + St = + + yt xt + +Another interesting feature of this type of pools is that knowing in an instant t 0 the following data: + +- the constant of the pool L; + + +- the number of token x at time t 0 given by x 0 ; + +- the number of token y at time t 0 given by y 0 ; + +and supposing that there is no injection of new capital in the pool till time T , we can calculate the number of tokens in every time t ∈ [t 0 , T ] as: + + xt = x 0 + +#### √ + +#### S 0 + + St + + yt = y 0 + +#### √ + + St S 0 + +#### (1. 3 .1) + +Having established these properties we can start analyzing how the position of a LP evolves in the pool. At time t 0 , that we will assume without losing of generality that is equal to 0, he deposits a certain quantity of token x and y given by: + + x 0 = + +#### L + +#### √ + +#### S 0 + + y 0 = L + +#### √ + +#### S 0 + +Doing so we have that the constant product is respected. So we can calculate the initial capital invested, in terms of token y, as: + + V 0 = x 0 S 0 + y 0 = 2L + +#### √ + +#### S 0 + +When the price moves the quantity of each token changes according to (1.3.1) so that we have: + + xt = + +#### L + +#### √ + + St + + yt = L + +#### √ + + St + +And so it also changes the value of the position: + + VLP (t) = xtSt + yt = 2L + +#### √ + + St = V 0 + +#### √ + + St S 0 + +#### = V 0 + +#### √ + + αt + +Where we have defined αt as the ratio of the change in price. So the P&L of the LP position at time t is: LP (t) = Vt − V 0 = V 0 ( + +#### √ + + αt − 1) + +What we have ignored till now are the fees that Traders have to pay when they do their swaps. Part of these fees go to the protocol while the remainder is divided between the LPs proportional to the quantity of liquidity they have provided. The fees can be expressed as: + + Φ(t) = V 0 · φ · t + +Where φ is the expected APY on the specific Liquidity Pool so that the actual payoff should be: + + VLP (t) = V 0 + +#### √ + + αt + Φ(t) = V 0 ( + +#### √ + + αt + φt) (1. 3 .2) + +While the final P&L is: LP (t) = V 0 ( + +#### √ + + αt + φt − 1) + +### 1.4 HODLer + +Before moving on we introduce one last character: the HODLer. In DeFi jargon an HODLer indicates a user that holds its tokens without doing anything. That is to say that given an initial quantity of tokens: + + x 0 and y 0 + +at every time t the HODLer will have: + + xt = x 0 and yt = y 0 + +Thus the position value of an HODLer is determined just by the change in price of the tokens. The position value of an HODLer expressed in terms of token y is given by: + + VH (t) = x 0 St + y 0 + + +## 2 Impermanent Loss + +Impermanent Loss (IL) is a known problem affecting the LPs defined as the difference between the return of a LP and that of an equal-weight (with respect to the starting time) HODLer. For a more in depth analysis of it on UniSwap, the most used constant product AMM, see [7] and [8]. We recall the starting quantities of tokens for a LP and hence of an equal-weight HODLer: + + x 0 = + +#### L + +#### √ + +#### S 0 + + y 0 = L + +#### √ + +#### S 0 + +and the values of a LP position and that of an HODLer at time t: + + VLP (t) = xtSt + yt = + + LSt √ St + +#### + L + +#### √ + + St = V 0 + +#### √ + + αt + + VH (t) = x 0 St + y 0 = + + LSt √ S 0 + +#### + L + +#### √ + +#### S 0 = V 0 + +#### ( + + αt + 1 2 + +#### ) + +Now we can compute the IL: + + IL(t) = VLP (t) − V 0 V 0 + +#### − + + VH (t) − V 0 V 0 + +#### = + + VLP (t) − VH (t) V 0 + +#### = + +#### √ + + αt − αt 2 + +#### − + +#### 1 + +#### 2 + +#### (2.1) + +Formula (2) is the same found in [8]. We can also express that in terms of the token return defining: + + rt := + + St S 0 + + − 1 = αt − 1 + +Doing so we obtain the following equivalent form: + + IL(r) = + +#### √ + + r + 1 − + + r 2 + +#### − 1 (2.2) + +-1.2 -1 (^0 1) Percentage Return 2 3 4 5 -1 -0.8 -0.6 -0.4 -0.2 0 Percentage Loss **Impermanent Loss** We plotted the IL given by formula (2.2). We note that IL(r) ≤ 0 and equal to zero only if the price of token x is exactly the same as its starting price S 0 (r = 0). This tells us that being a LP is always worse than being an HODLer, unless the fees are enough to offset this difference. + +### 2.1 LP as an option seller + +It has been noted, for example in [9], that being a LP, if we consider the IL, is actually the same as being an option seller. In fact we know that we can replicate any given twice differentiable payoff h(x) via the formula: + + + h(ST ) = h(S 0 ) + h′(S 0 )(ST − S 0 ) + + +∫ (^) S 0 0 h′′(K)(K − ST )+dK + ∫ (^) ∞ S 0 h′′(K)(ST − K)+dK In our case we have that our payoff function h is the IL that is: h(x) = + +#### √ + + x S 0 + +#### − + + x 2 S 0 + +#### − + +#### 1 + +#### 2 + +clearly this function is C∞(R+) so that we can apply the replication formula. First we will compute the first and second derivative of the function:      + + h′(x) = + +#### 1 + +#### 2 + +#### √ + + xS 0 + +#### − + +#### 1 + +#### 2 S 0 + + h′′(x) = − + +#### 1 + + 4 x^3 /^2 + +#### √ + +#### S 0 + +#### < 0 + +Thus substituting we obtain: + + h(ST ) = − + +∫ (^) S 0 0 + +#### 1 + +#### 4 K^3 /^2 + +#### √ + +#### S 0 + + (K − ST )+dK − + +∫ (^) ∞ S 0 + +#### 1 + +#### 4 K^3 /^2 + +#### √ + +#### S 0 + + (ST − K)+dK + +That is to say that we can replicate the IL selling an infinite strip of puts and calls of all strikes with maturity T. + + +## 3 LP pricing and Greeks + +On most DEXs like Uniswap, LPs can withdraw their liquidity at any time by redeeming their LP tokens. In this case, the price of the LP position is, as seen in formula (1.3.2), always equal to the value of the underlying assets plus fees: + + Pt = V 0 ( + +#### √ + + rt + 1 + φt) = V 0 + +#### (√^ + + St S 0 + + + φt + +#### ) + +Where rt is the return of asset x relative to asset y and φ is the expected APY. + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 5000 10000 15000 Pt **LP Position Price** As we will see below, unlocked liquidity has a positive delta, negative gamma and positive theta, while the vega exposure is zero. This is particularly important when options are used to hedge the position as suggested in [5] and [6]. Options do have a vega exposure, therefore an unlocked LP portfolio combined with a long options position always has a positive vega. + +### 3.1 Unlocked Liquidity Greeks + +3.1.1 Delta + +Delta is defined as the partial derivative of the price of the position (Pt) with respect to the underlying price (St): + +#### ∆LP := + + ∂Pt ∂St + +#### = + +#### V 0 + +#### 2 + +#### √ + + S 0 St + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 20 40 60 80 100 120 140 160 LP **Delta LP** + + +3.1.2 Delta 1% + +Delta 1% is defined as the change in price of the position when the underlying price changes by 1%. So we will compute it as: + + ∆1% LP (St) := ∆LP (St) · + + St 100 + +#### = + +#### V 0 + +#### 2 + +#### √ + + St S 0 + +#### · 10 −^2 + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 10 20 30 40 50 60 70 80 1% LP **Delta 1% LP** 3.1.3 Gamma Gamma is defined as the second partial derivative of the price of the position (Pt) with respect to the underlying price (St). It can also be seen as the partial derivative of Delta with respect to the underlying price: + +#### ΓLP := + + ∂^2 Pt ∂S t^2 + +#### = + +#### ∂∆LP + + ∂St + +#### = − + +#### V 0 + +#### 4 + +#### √ + +#### S 0 S + + 3 / 2 t + +We note that ΓLP < 0 and that ΓLP → −∞ when St → 0. + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -2.5 -2 -1.5 -1 -0.5 0 LP **Gamma LP** + + +3.1.4 Gamma 1% + +Gamma 1% is defined as the change of the Delta 1% when the underlying price changes by 1%. So we will compute it as: + + Γ1% LP (St) = ΓLP (St) · + +#### ( + + St 100 + +#### ) 2 + +#### = − + +#### V 0 + +#### 4 + +#### √ + + St S 0 + +#### · 10 −^4 + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -0.4 -0.35 -0.3 -0.25 -0.2 -0.15 -0.1 -0.05 0 1% LP **Gamma 1% LP** 3.1.5 Vega Vega is defined as the partial derivative of the price of the position with respect to the volatility (σ): νLP := ∂Pt ∂σ + +#### = 0 + +3.1.6 Theta + +Theta is defined as the partial derivative of the price of the position (Pt) with respect to time (t): + +#### ΘLP := + + ∂Pt ∂t + + = φ · V 0 + +3.1.7 Rho + +Rho is defined as the partial derivative of the price of the position (Pt) with respect to the risk-free rate (rf ): + + ρLP := + + ∂Pt ∂rf + +#### = 0 + +### 3.2 Locked Liquidity Analysis + +From here onwards, we assume that the Liquidity Provider has locked his liquidity until time T , perhaps to access liquidity mining rewards. When he does so, he will no longer be able to redeem the underlying assets until maturity. In this case, the fair price of his position may differ from the value of the underlying assets. As we will see, while unlocked liquidity gives exposure only to delta, gamma and theta, locking the liquidity exposes the holder to additional vega and rho risks. In order to calculate the fair price of the locked LP position we need to know: + +- the maturity T when the liquidity gets unlocked (in years); + +- the current time t (in years); + + +- the remaining time τ = T − t (in years); + +- the starting price for the underlying S 0 ; + +- the initial capital invested V 0. + +Similar to Black & Scholes, we assume that the price processes are governed by the following SDEs: + + dBtx = rxBtx dt dByt = ry Bty dt dSt = μStdt + σStdWt + +Where Bxt is the "bond" process relative to token x with risk free rate rx (we can take for example the APY on lending token x on Aave), Byt is the "bond" process relative to token y with risk free rate ry (analogously we can take the APY on lending token y on Aave), St is the market price of token x in terms of token y determined by the drift μ, the volatility σ and the Brownian Motion (BM) Wt. So this is to say that the price process is a Geometric Brownian Motion (GBM). It is known that we can find a probability Q (risk-free probability) in which we have that the price process follows the following SDE: + + dSt = (rx − ry )Stdt + σd W˜t + +Where we have that W˜t is another BM given by the Ito formula: + + d W˜t = dWt − + + μ − (rx − ry ) σ + + dt + +We can solve the price process SDE and defining rf := rx − ry we obtain: + + St = S 0 exp + +#### { + + rf t − + +#### 1 + +#### 2 + + σ^2 t + σ W˜t + +#### } + +We will assume that the liquidity pools are efficient, thanks to the presence of arbitrageurs, so that the pool price of token x in terms of token y is arbitrarily close to the market price, except at most in some instants, so that we can use the market price dynamics also for the pool price dynamics. From here onward we will denote everything in terms of token y. Now we can find the price of a locked liquidity position computing the discounted payoff under Q: + + Pt = EQ[e−rf^ τ^ VLP (T )] = e−rf^ τ^ EQ + +#### [ + +#### V 0 + +#### √ + +#### S 0 + +#### √ + +#### ST + Φ(T ) + +#### ] + + = e−rf^ τ + +#### ( + +#### V 0 + +#### √ + +#### S 0 + +#### EQ[ + +#### √ + +#### ST ] + Φ(T ) + +#### ) + +Thanks to the property of the GBM we have that: + +#### EQ[ + +#### √ + +#### ST ] = + +#### √ + + St exp + +#### { + +#### 1 + +#### 2 + + rf τ − + +#### 1 + +#### 8 + + σ^2 τ + +#### } + +#### (⋆) + +See Appendix A for the full derivation of (⋆). Substituting this into the pricing formula and doing some rearrangements we obtain: + + Pt = V 0 + +#### (√^ + + St S 0 + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + + + φT e−rf^ τ + +#### ) + +This is the fair value of the LP position locked until time T , when the expected volatility on the pair of tokens is σ and φ is the expected APY of the pool. + + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + 0 + + 5000 + + 10000 + + 15000 + + Pt + + LP Position Price + +Plot obtained with the following data: + +- V 0 = 10000, S 0 = 1000; + +- rf = 3%, σ = 70%, φ = 10%; + +- T = 0. 5 , τ = 0. 25. + +All the plots in this section are obtained using the same data. + +### 3.3 Greeks + +3.3.1 Delta + +#### ∆LP = + +#### V 0 + +#### 2 + +#### √ + + S 0 St + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + +We note that ∆LP > 0. + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 20 40 60 80 100 120 140 160 LP **Delta LP** We can see that the delta of a liquidity provider increases when the underlying price S drops and vice versa. + + +3.3.2 Delta 1% + + ∆1% LP (St) = + +#### V 0 + +#### 2 + +#### √ + + St S 0 exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + +#### · 10 −^2 + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 10 20 30 40 50 60 70 1% LP **Delta 1% LP** 3.3.3 Gamma + +#### ΓLP = − + +#### V 0 + +#### 4 + +#### √ + +#### S 0 S + + 3 / 2 t + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + +We note that ΓLP < 0 and that ΓLP → −∞ when St → 0. + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -2.5 -2 -1.5 -1 -0.5 0 LP **Gamma LP** 3.3.4 Gamma 1% Γ1% LP (St) = − + +#### V 0 + +#### 4 + +#### √ + + St S 0 + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + +#### · 10 −^4 + + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -0.35 + + -0.3 + + -0.25 + + -0.2 + + -0.15 + + -0.1 + + -0.05 + + 0 + + 1% LP + + Gamma 1% LP + +3.3.5 Vega + + νLP = −V 0 + + στ 4 + +#### √ + + St S 0 + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + +We note that νLP < 0. + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -7 + + -6 + + -5 + + -4 + + -3 + + -2 + + -1 + + 0 + + LP + + Vega LP + +In the figure we have plotted the Vega corresponding to a 1% change in volatility σ, that is ν/ 100. + +3.3.6 Theta + +#### ΘLP = V 0 + +#### (√^ + + St S 0 + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + + + rf φT e−rf^ τ + +#### ) + + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + 0 + + 0.5 + + 1 + + 1.5 + + 2 + + 2.5 + + 3 + + LP + + Theta LP + +In the figure we have plotted the daily Theta, that is Θ/ 365. + +3.3.7 Rho + +Even if we have a model with two risk free rates we note that the price depends only on their difference rf so we can study only the sensibility with respect to rf. + + ρLP := ∂Pt ∂rf + +#### = −V 0 + +#### ( + + τ 2 + +#### √ + + St S 0 + + exp + +#### { + + − τ + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### )} + + + τ φT e−rf^ τ + +#### ) + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -20 + + -18 + + -16 + + -14 + + -12 + + -10 + + -8 + + -6 + + -4 + + -2 + + 0 + + LP + + Rho LP + +In the figure we have plotted the Rho corresponding to a 1% change of rf , that is ρ/ 100. + + +## 4 Impermanent Gain + +As detailed in the previous section, a LP is exposed to many risks. While it is relatively easy to hedge the Delta, for example shorting futures on the underlying S, we ask ourselves if we can structure a product that hedges all the other greeks, in particular Vega, Gamma and Theta. We call this product Impermanent Gain (IG). IG’s unit payoff is defined as the opposite of IL: + + IG(r) = + +#### VH − VLP + +#### V 0 + +#### = 1 + + + r 2 + +#### − + +#### √ + + r + 1 + +The IG is a product that has some similarities with European options: + +- it has a maturity T ; + +- it has a strike K that is the starting price from which the IG is computed: + + rt = + + St K + +#### − 1 + + where rt is used to compute the IG at time t. + +IG payoff with maturity T can be coded into a smart contract making it a DeFi-native product. + +### 4.1 Pricing + +We have seen in Section 2.1 that we can replicate the IL selling an infinite strip of puts and calls and so we can also replicate the IG buying the same portfolio, thus we could price the IG position finding the cost of the replicating portfolio. Instead of doing that we will use a different approach giving a dynamic for the price process of token x, in terms of token y, and using as the price for the IG position the discounted payoff. We will assume the same dynamics and conditions described in Section 3.2 for the market and liquidity pool price processes. Given the following data: + +- maturity T (in years); + +- current time t (in years); + +- time to expiry τ = T − t (in years); + +- strike K = S 0 , that is the starting price of the token; + +- initial capital V 0 ; + +we can calculate the price of the IG strategy at time t as the discounted payoff under the risk-free measure Q: + + Pt = EQ[e−rf^ τ^ · V 0 · IG(ST )] = EQ + +#### [ + + e−rf^ τ^ V 0 + +#### ( + +#### 1 + +#### 2 + +#### + + +#### ST + +#### 2 K + +#### − + +#### √ + +#### ST + +#### K + +#### )] + + Pt = e−rf^ τ^ V 0 + +#### ( + +#### 1 + +#### 2 + +#### + + +#### 1 + +#### 2 K + +#### EQ[ST ] − + +#### 1 + +#### √ + +#### K + +#### EQ[ + +#### √ + +#### ST ] + +#### ) + +Remembering the properties of the GBM we have: + + EQ[ST ] = Sterf^ τ^ (•) + +#### EQ[ + +#### √ + +#### ST ] = + +#### √ + + St exp + +#### { + +#### 1 + +#### 2 + + rf τ − + +#### 1 + +#### 8 + + σ^2 τ + +#### } + +See Appendix B for the full derivation of (•). Substituting into the price formula and doing some rearrangements we obtain: + + Pt = V 0 + +#### ( + +#### 1 + +#### 2 + + e−rf^ τ^ + + + St 2 K + +#### − + +#### √ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### }) + + + 0 500 1000 1500 2000 2500 3000 3500 4000 St + + 0 + + 1000 + + 2000 + + 3000 + + 4000 + + 5000 + + 6000 + + Pt + + IG Position Price + +Plot obtained with the following data: + +- V 0 = 10000, K = 1000; + +- rf = 3%, σ = 70%; + +- τ = 3657 (seven days). + +All the plots in this section are obtained using the same data. + +### 4.2 Greeks + +4.2.1 Delta + +#### ∆IG = V 0 + +#### ( + +#### 1 + +#### 2 K + +#### − + +#### 1 + +#### 2 + +#### √ + + KSt + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### }) + +We note that the ∆IG(K) > 0. + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -160 + + -140 + + -120 + + -100 + + -80 + + -60 + + -40 + + -20 + + 0 + + 20 + + IG + + Delta IG + + +4.2.2 Delta 1% + + ∆1% IG (St) = V 0 + +#### ( + + St 2 K + +#### − + +#### 1 + +#### 2 + +#### √ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### }) + +#### · 10 −^2 + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -15 + + -10 + + -5 + + 0 + + 5 + + 10 + + 15 + + 20 + + 25 + + 30 + + 1% IG + + Delta 1% IG + +4.2.3 Gamma + +#### ΓIG = + + ∂^2 Pt ∂S^2 t + +#### = + +#### V 0 + +#### 4 + +#### √ + + KS t^3 /^2 + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + +We note that ΓIG > 0 and that ΓLP → +∞ when St → 0. + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 0.5 1 1.5 2 2.5 IG **Gamma IG** 4.2.4 Gamma 1% Γ1% IG (St) = + +#### V 0 + +#### 4 + +#### √ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + +#### · 10 −^4 + + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + 0 + + 0.05 + + 0.1 + + 0.15 + + 0.2 + + 0.25 + + 0.3 + + 0.35 + + 0.4 + + 1% IG + + Gamma 1% IG + +4.2.5 Vega + + νIG = V 0 + + στ 4 + +#### √ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 IG **Vega IG** In the figure we have plotted the Vega corresponding to a 1% change in volatility σ, that is ν/ 100. 4.2.6 Theta + +#### ΘIG = V 0 + +#### ( + + rf 2 + + e−rf^ τ^ − + +#### √ + + St K + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### }) + + + 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St + + -3 + + -2.5 + + -2 + + -1.5 + + -1 + + -0.5 + + 0 + + 0.5 + + IG + + Theta IG + +In the figure we have plotted the daily Theta, that is Θ/ 365. + +4.2.7 Rho + +ρIG = − + + V 0 τ 2 + + e−rf^ τ^ + + + V 0 τ 2 + +#### √ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + +#### = + + V 0 τ 2 + +#### (√^ + + St K + + exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + + − e−rf^ τ + +#### ) + +(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 IG **Rho IG** In the figure we have plotted the Rho corresponding to a 1% change of rx, that is ρ/ 100. + +### 4.3 Impermanent Gain as a Hedging Tool + +Imagine an LP provides $ 10000 of full-range liquidity on Uniswap on the ETH/USDC pair. He then locks his liquidity for 1 year using a liquidity mining platform. ETH price is $ 1000. He wants to hedge his position using Impermanent Gain. We have: + +- V 0 = 10000, S 0 = 1000; + +- rf = 3%, σ = 70%, φ = 10%; + + +- T = 1, τ = 1. + +For the IG, the LP chooses a strike K equal to S 0 = 1000 and a maturity of 1 year. We can compute the greeks of this position. + +4.3.1 Delta + +#### ∆ = ∆IG + ∆LP = + +#### V 0 + +#### 2 K + +We note that the Delta is a constant determined by the invested capital V 0 and the strike K. In our case we have ∆ = 5. + +4.3.2 Gamma + + Γ = ΓIG + ΓLP = 0 + +Doing this hedge the resulting position is a Gamma-neutral one. + +4.3.3 Vega + + ν = νIG + νLP = 0 + +Doing this hedge the resulting position is a Vega-neutral one. + +4.3.4 Theta + + ΘIG + ΘLP = V 0 rf + +#### ( + +#### 1 + +#### 2 + + + φT + +#### ) + + e−rf^ τ + +We note that now Theta doesn’t depend on the underlying price St and on the volatility σ. + +4.3.5 Rho + + ρ = ρIG + ρLP = −V 0 τ + +#### ( + +#### 1 + +#### 2 + + + φT + +#### ) + + e−rf^ τ + +We note that now Rho doesn’t depend on the underlying price St and on the volatility σ. + +As we have seen, for a liquidity provider with locked liquidity, buying Impermanent Gain completely eliminates Gamma and Vega risks as well as significantly reduces Theta and Rho. + +## 5 Conclusion + +In this paper we analyzed the risks of a liquidity provider with a focus on Impermanent Loss, detailing the position greeks under Black & Scholes assumptions. We found that a liquidity provider has a positive Delta, negative Gamma and positive Theta, while the Vega is zero; we also demonstrated that locking the liquidity changes the risk profile of a liquidity provider and introduces a negative Vega risk. Additionally, we introduced Impermanent Gain, a DeFi-native product tailored for liquidity providers’ needs and demonstrated how it can be used to eliminate most financial risks related to providing liquidity. + + +## Appendix A + +Recall that: + + ST = St exp + +#### {( + + rf − + + σ^2 2 + +#### ) + + τ + σWτ + +#### } + + Wτ ∼ N (0, τ ) + +Recall also that given a standard normal distribution Z ∼ N (0, 1) we have that: + +#### E + +#### [ + + exp{uZ} + +#### ] + + = exp + +#### { + + u^2 2 + +#### } + +Now we can proceed: + +#### E[ + +#### √ + +#### ST ] = E + +#### [√ + + St exp + +#### { + +#### 1 + +#### 2 + +#### ( + + rf − σ^2 2 + +#### ) + + τ + σ 2 + + Wτ + +#### }] + +#### = + +#### √ + + St exp + +#### { + +#### 1 + +#### 2 + +#### ( + + rf − σ^2 2 + +#### ) + + τ + +#### } + +#### E + +#### [ + + exp + +#### { + + σ 2 + +#### √ + + τ Z + +#### }] + +#### =⇒ E[ + +#### √ + +#### ST ] = + +#### √ + + St exp + +#### { + + rf 2 + + τ − + + σ^2 4 + + τ + +#### } + + exp + +#### { + + σ^2 8 + + τ + +#### } + +#### = + +#### √ + + St exp + +#### {( + + rf 2 + +#### − + + σ^2 8 + +#### ) + + τ + +#### } + +## Appendix B + +Recall that: + + ST = St exp + +#### {( + + rf − + + σ^2 2 + +#### ) + + τ + σWτ + +#### } + + Wτ ∼ N (0, τ ) + +Recall also that given a standard normal distribution Z ∼ N (0, 1) we have that: + +#### E + +#### [ + + exp{uZ} + +#### ] + + = exp + +#### { + + u^2 2 + +#### } + +Now we can proceed: + +#### E[ST ] = E + +#### [ + + St exp + +#### {( + + rf − σ^2 2 + +#### ) + + τ + σWτ + +#### }] + + = St exp + +#### {( + + rf − σ^2 2 + +#### ) + + τ + +#### } + +#### E + +#### [ + + exp + +#### { + + σ + +#### √ + + τ Z + +#### }] + + =⇒ E[ST ] = St exp + +#### { + + rf τ − + + σ^2 2 + + τ + +#### } + + exp + +#### { + + σ^2 2 + + τ + +#### } + + = Sterf^ τ + + +## Appendix C + +The following table is a Greeks comparison between the different strategies. Let’s first define: + + β := exp + +#### { + +#### − + +#### ( + + rf 2 + +#### + + + σ^2 8 + +#### ) + + τ + +#### } + + γ := e−rf^ τ + + Greeks Unlocked LP Locked LP Impermanent Gain + + ∆ 2 √VS^00 St 2 √^ VS^00 St β V 0 + +#### ( + + 1 2 K −^ + + β 2 √KSt + +#### ) + +#### ∆1%^ V 20 + +#### √ + + St S 0 ·^10 + + − 2 V 0 2 + +#### √ + + St S 0 β^ ·^10 + +− (^2) V 0 + +#### ( + + St 2 K −^ + + 1 2 + +#### √ + + St K β + +#### ) + +#### · 10 −^2 + +#### Γ − V^0 + + 4 √ S 0 S^3 t/^2 + +#### − V^0 + + 4 √ KS t^3 /^2 β ∂ + +(^2) Pt ∂S^2 t^ =^ V 0 4 √ KS^3 t/^2 β Γ1%^ − V 40 + +#### √ + + St S 0 ·^10 + +− (^4) − V 0 4 + +#### √ + + St S 0 β^ ·^10 + + − 4 V 0 4 + +#### √ + + St K β^ ·^10 + + − 4 + + ν 0 −V 0 στ 4 + +#### √ + + St S 0 β^ V^0 + + στ 4 + +#### √ + + St K β Θ φ · V 0 V 0 + +#### (√ + + St S 0 + +#### ( + + rf 2 +^ + + σ^2 8 + +#### ) + + β + rf φT γ + +#### ) + +#### V 0 + +#### ( + + rf 2 γ^ − + +#### √ + + St K + +#### ( + + rf 2 +^ + + σ^2 8 + +#### ) + + β + +#### ) + + ρ 0 −V 0 + +#### ( + + τ 2 + +#### √ + + St S 0 β^ +^ τ φT γ + +#### ) + + V 0 τ 2 + +#### ( + + β + +#### √ + + St K −^ γ + +#### ) + + +## References + +1. Angeris, Chitra, "Improved Price Oracles: Constant Function Market Makers", June 2020, arXiv:2003.10001 [q-fin.TR] + +2. Jensen, Pourpouneh, Nielsen, Ross, "THE HOMOGENEOUS PROPERTIES OF AUTOMATED MARKET MAKERS", arXiv:2105.02782 [q-fin.TR] + +3. Park, Andreas, "Conceptual Flaws of Decentralized Automated Market Making" (April 11, 2022). Available at SSRN: https://ssrn.com/abstract=3805750 or [http://dx.doi.org/10.2139/ssrn.3805750](http://dx.doi.org/10.2139/ssrn.3805750) + +4. Clark, Joseph, "The Replicating Portfolio of a Constant Product Market" (March 8, 2020). Available at SSRN: https://ssrn.com/abstract=3550601 or [http://dx.doi.org/10.2139/ssrn.3550601](http://dx.doi.org/10.2139/ssrn.3550601) + +5. Adams, Robinson, Zinsmeister, "Uniswap v2 Core", March 2020, available at: https://uniswap.org/whitepaper.pdf + +6. Adams, Keefer, Robinson, Salem, Zinsmeister, "Uniswap v3 Core", March 2021, available at: https://uniswap.org/whitepaper-v3.pdf + +7. Aigner, Dhaliwal, "UNISWAP: Impermanent Loss and Risk Profile of a Liquidity Provider", June 2021, arXiv:2106.14404 [q-fin.TR] + +8. Elsts, "LIQUIDITY MATH IN UNISWAP V3", 30 September 2021, available at: https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf + +9. Fukasawa, Masaaki and Maire, Basile and Wunsch, Marcus, Weighted variance swaps hedge against Impermanent Loss (April 27, 2022). Available at SSRN: https://ssrn.com/abstract=4095029 or [http://dx.doi.org/10.2139/ssrn.4095029](http://dx.doi.org/10.2139/ssrn.4095029) + +10. Black, Scholes, "The Pricing of Options and Corporate Liabilities", Journal of Political Economy Vol. 81, No. 3 (May - Jun, 1973), pp. 637-654, The University of Chicago Press + + diff --git a/test/samples/URL Sample.md b/test/samples/URL Sample.md new file mode 100644 index 00000000..a952f62d --- /dev/null +++ b/test/samples/URL Sample.md @@ -0,0 +1,7 @@ +# Sample PDF + +## This is a simple PDF file. Fun fun fun. + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus facilisis odio sed mi. Curabitur suscipit. Nullam vel nisi. Etiam semper ipsum ut lectus. Proin aliquam, erat eget pharetra commodo, eros mi condimentum quam, sed commodo justo quam ut velit. Integer a erat. Cras laoreet ligula cursus enim. Aenean scelerisque velit et tellus. Vestibulum dictum aliquet sem. Nulla facilisi. Vestibulum accumsan ante vitae elit. Nulla erat dolor, blandit in, rutrum quis, semper pulvinar, enim. Nullam varius congue risus. Vivamus sollicitudin, metus ut interdum eleifend, nisi tellus pellentesque elit, tristique accumsan eros quam et risus. Suspendisse libero odio, mattis sit amet, aliquet eget, hendrerit vel, nulla. Sed vitae augue. Aliquam erat volutpat. Aliquam feugiat vulputate nisl. Suspendisse quis nulla pretium ante pretium mollis. Proin velit ligula, sagittis at, egestas a, pulvinar quis, nisl. Pellentesque sit amet lectus. Praesent pulvinar, nunc quis iaculis sagittis, justo quam lobortis tortor, sed vestibulum dui metus venenatis est. Nunc cursus ligula. Nulla facilisi. Phasellus ullamcorper consectetuer ante. Duis tincidunt, urna id condimentum luctus, nibh ante vulputate sapien, id sagittis massa orci ut enim. Pellentesque vestibulum convallis sem. Nulla consequat quam ut nisl. Nullam est. Curabitur tincidunt dapibus lorem. Proin velit turpis, scelerisque sit amet, iaculis nec, rhoncus ac, ipsum. Phasellus lorem arcu, feugiat eu, gravida eu, consequat molestie, ipsum. Nullam vel est ut ipsum volutpat feugiat. Aenean pellentesque. In mauris. Pellentesque dui nisi, iaculis eu, rhoncus in, venenatis ac, ante. Ut odio justo, scelerisque vel, facilisis non, commodo a, pede. Cras nec massa sit amet tortor volutpat varius. Donec lacinia, neque a luctus aliquet, pede massa imperdiet ante, at varius lorem pede sed sapien. Fusce erat nibh, aliquet in, eleifend eget, commodo eget, erat. Fusce consectetuer. Cras risus tortor, porttitor nec, tristique sed, convallis semper, eros. Fusce vulputate ipsum a mauris. Phasellus mollis. Curabitur sed urna. Aliquam nec sapien non nibh pulvinar convallis. Vivamus facilisis augue quis quam. Proin cursus aliquet metus. Suspendisse lacinia. Nulla at tellus ac turpis eleifend scelerisque. Maecenas a pede vitae enim commodo interdum. Donec odio. Sed sollicitudin dui vitae justo. Morbi elit nunc, facilisis a, mollis a, molestie at, lectus. Suspendisse eget mauris eu tellus molestie cursus. Duis ut magna at justo dignissim condimentum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus varius. Ut sit amet diam suscipit mauris ornare aliquam. Sed varius. Duis arcu. Etiam tristique massa eget dui. Phasellus congue. Aenean est erat, tincidunt eget, venenatis quis, commodo at, quam. + + From c739f54e0068fad2ea80ba82fb1e317ee924d791 Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 10 Dec 2025 16:00:48 +0200 Subject: [PATCH 4/5] fix: getOutputSinceSnapshot now checks completedSessions as fallback When a process finishes between taking an output snapshot and polling for new output, getOutputSinceSnapshot was returning null (session not found), causing interactWithProcess to miss the final output and treat it as a timeout with no output. Now checks completedSessions when active session is not found, similar to how readOutputPaginated already handles this case. --- src/terminal-manager.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index 98005c53..0c902c43 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -553,17 +553,30 @@ export class TerminalManager { /** * Get output that appeared since a snapshot was taken. * This handles the case where output is appended to the last line (REPL prompts). + * Also checks completed sessions in case process finished between snapshot and poll. */ getOutputSinceSnapshot(pid: number, snapshot: { totalChars: number; lineCount: number }): string | null { + // Check active session first const session = this.sessions.get(pid); - if (!session) return null; + if (session) { + const fullOutput = session.outputLines.join('\n'); + if (fullOutput.length <= snapshot.totalChars) { + return ''; // No new output + } + return fullOutput.substring(snapshot.totalChars); + } - const fullOutput = session.outputLines.join('\n'); - if (fullOutput.length <= snapshot.totalChars) { - return ''; // No new output + // Fallback to completed sessions - process may have finished between snapshot and poll + const completedSession = this.completedSessions.get(pid); + if (completedSession) { + const fullOutput = completedSession.outputLines.join('\n'); + if (fullOutput.length <= snapshot.totalChars) { + return ''; // No new output + } + return fullOutput.substring(snapshot.totalChars); } - return fullOutput.substring(snapshot.totalChars); + return null; } /** From 450d1aba0d0549216b5887f1f299ac622f6da0df Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Wed, 10 Dec 2025 16:11:55 +0200 Subject: [PATCH 5/5] Cleanup --- docs/privacy-policy-update-meeting.md | 150 -- test/samples/01_sample_simple.pdf.md | 13 - test/samples/02_sample_invoice.pdf.md | 37 - .../page_undefined_img_1.webp | Bin 4390 -> 0 bytes .../page_undefined_img_2.webp | Bin 736 -> 0 bytes test/samples/03_sample_compex.pdf.md | 2142 ----------------- test/samples/URL Sample.md | 7 - 7 files changed, 2349 deletions(-) delete mode 100644 docs/privacy-policy-update-meeting.md delete mode 100644 test/samples/01_sample_simple.pdf.md delete mode 100644 test/samples/02_sample_invoice.pdf.md delete mode 100644 test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp delete mode 100644 test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp delete mode 100644 test/samples/03_sample_compex.pdf.md delete mode 100644 test/samples/URL Sample.md diff --git a/docs/privacy-policy-update-meeting.md b/docs/privacy-policy-update-meeting.md deleted file mode 100644 index 04bfc282..00000000 --- a/docs/privacy-policy-update-meeting.md +++ /dev/null @@ -1,150 +0,0 @@ -# Privacy Policy Update - Team Discussion - -**Date:** December 8, 2025 -**PR:** https://github.com/wonderwhy-er/DesktopCommanderMCP/pull/287 -**Duration:** 15-20 minutes - ---- - -## Context: Why This Update? - -1. **Code audit revealed inconsistencies** between what the privacy policy said and what the code actually does -2. **Australian Privacy Act complaint** highlighted specific issues (APP 1, 2, 3, 5) -3. **No changes to actual data collection** — this is about making the policy accurate, not changing behavior - ---- - -## Summary of Changes - -### 1. "Anonymous" → "Pseudonymous" - -**Before:** "Anonymous client ID... is not connected to any other telemetry event data" - -**After:** "Pseudonymous client ID... is included with telemetry events" - -**Why:** The UUID is sent with every event (we need this for retention metrics). Calling it "anonymous" and "isolated" was factually incorrect. Under GDPR, pseudonymous data is still personal data. - ---- - -### 2. Removed "No PII" Claim - -**Before:** "avoiding any personally identifiable information (PII)" - -**After:** Lists specifically what we don't collect (names, emails, usernames, file paths) - -**Why:** -- Under US law: UUID might not be PII -- Under GDPR: UUID IS personal data -- Safer to be specific about what we don't collect rather than make broad claims - ---- - -### 3. Added Missing Data Fields - -**Added disclosures for:** -- Client info (Claude Desktop, VS Code version) -- Container/Docker metadata -- File sizes -- Runtime source - -**Why:** These were being collected but not documented. Full transparency. - ---- - -### 4. Named Google Analytics Explicitly - -**Before:** "sent securely via HTTPS to Google Analytics" (buried in text) - -**After:** Dedicated "Analytics Provider" section naming GA4 - -**Why:** Industry standard for developer tools. Users expect to know who processes their data. - ---- - -### 5. IP Address Clarification - -**Added:** "We do not store IP addresses. However, Google Analytics receives them via HTTPS and auto-anonymizes them." - -**Why:** We can't claim "we don't collect IPs" when our analytics provider sees them. This is honest about the technical reality. - ---- - -### 6. Added "Your Rights" Section - -**New section includes:** -- List of GDPR rights (access, deletion, objection, withdraw consent) -- How to find your UUID (ask AI or check config file) -- Explanation that we can only process requests with UUID -- 30-day response commitment - -**Why:** -- GDPR/Australian Privacy Act require this -- Explains the privacy paradox: we're SO private we can't identify users -- This is actually a strength, not a weakness - ---- - -### 7. Added Legal Contact Email - -**Added:** legal@desktopcommander.app for privacy/legal matters - -**Why:** -- GitHub issues are public — not appropriate for legal matters -- Required for compliance -- We already have this email, just wasn't in the policy - ---- - -### 8. Simplified README Section - -**Before:** ~25 lines of privacy details in README - -**After:** 5 lines + link to PRIVACY.md - -**Why:** Single source of truth, easier maintenance, README was already 960+ lines - ---- - -## Discussion Points - -### A. Are we comfortable with "pseudonymous" language? -- Legally accurate -- Might sound scarier than "anonymous" to some users -- But: honesty > marketing - -### B. The Australian complaint -- Changes address APP 1 (clear policy), APP 5 (proper notice) -- APP 2 (anonymity option) — we can argue UUID-based system IS effectively anonymous since we can't re-identify -- APP 3 (necessity) — retention metrics are legitimate business need - -### C. Should we add anything else? -- Children's data statement? ("Not directed at under-18s") -- Cross-border transfer note? (Data processed in US) -- More detailed retention explanation? - ---- - -## Decisions Needed - -1. ✅ / ❌ Approve PR as-is? -2. ✅ / ❌ Add children's data statement? -3. ✅ / ❌ Add cross-border transfer note? -4. ✅ / ❌ Update website privacy policy to match? - ---- - -## Files Changed - -| File | Changes | -|------|---------| -| `PRIVACY.md` | Major rewrite — all changes above | -| `README.md` | Simplified privacy section, links to PRIVACY.md | - ---- - -## Post-Meeting Actions - -- [ ] Merge PR -- [ ] Update https://legal.desktopcommander.app/privacy_desktop_commander_mcp -- [ ] Mention in next release notes -- [ ] Respond to Discord complaint with link to updated policy diff --git a/test/samples/01_sample_simple.pdf.md b/test/samples/01_sample_simple.pdf.md deleted file mode 100644 index 83532e5c..00000000 --- a/test/samples/01_sample_simple.pdf.md +++ /dev/null @@ -1,13 +0,0 @@ -# Hello World - -This is a test PDF created from markdown. - -## Features - - Simple text Bold text Italic text Link - -## Code - - console.log('Hello World'); - - diff --git a/test/samples/02_sample_invoice.pdf.md b/test/samples/02_sample_invoice.pdf.md deleted file mode 100644 index da79f375..00000000 --- a/test/samples/02_sample_invoice.pdf.md +++ /dev/null @@ -1,37 +0,0 @@ -## POWERED BY - -#### 1 - - Sub Total 20,000.00 Total Rs.20,000.00 - -### Balance Due Rs.20,000.00 - - Authorized Signature - - Total In Words Indian Rupee Twenty Thousand Only - - Notes - - Thanks for your business. - - Payment Options - - Terms & Conditions GST not applicable - -## Tattva Trails Private Limited - - Some street 12 Panjim Goa 999888 India 12344321 tattva.trails.official@gmail.com - -# TAX INVOICE - -#### # : INV-000007 - - Invoice Date : 20/11/2025 Terms : Due on Receipt Due Date : 20/11/2025 - - Bill To - -### Shanti Spa Ayurveda - - # Item & Description Qty Rate Amount 1 Referral Fee – Panchakarma Treatment Package (14 Days) 1.00 20,000.00 20,000.00 - - diff --git a/test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp b/test/samples/02_sample_invoice.pdf_images/page_undefined_img_1.webp deleted file mode 100644 index 8bd1acd3f0ea5618a970f79dd2cd75f264d5de43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4390 zcmV+>5!voiNk&E<5dZ*JMM6+kP&il$0000G0002T002q=06|PpNI(Vv00HoaZQJok z+V&kusjKaA+qT_3_Oxx=wr$(CZQHh0=2UWSe_ToGy!y^nx0fX%CP4mAb>fB#_ih_k zA>mi=EiSJ#_D|RiQn-KBFHHjOPld-3vvMSuQk7{wVcUrvLvy+EeGG7G`id|Y;M`Wp zywt_ME0NgBKCj$v#=%z#*Um-B40Qlz+C`Fee&_YknFb;*~RD&3&&cOjJr?=Eo69yhoCa#Q($bxm~``U=tCk7!54V>HwxnJd<~Q>_YR0 z^J_=7sGPySzyXEXx4!Wfz6bDBz1TmkO~v%$@l@dcbik1%J|aG~PRGyj|(Hoa>_qUI0S~7)b{0ne|0~L)Tecc}njPeeN zJ_0dA3j~PvTz~>7t4F?$bGKG>+Ft@nsidOW_Iof89N-mx1ZW^;`o^LRXGC)w!1>g` z&pHZWfC9Qi>Ht*q3cY}*V(xVmKy_WyfaCVY(ZE3+e*sFtU;6xFfu>$zAaKc~waReM zu4^{1L!>Vd7pS2&u6yZ|Frc+pXbyzg%o(Bp+KcXiz)g`n0NG5KV*zCVzA>k^;bRhQ z^?)dw6g6ZvVZQ~WQowiSR5Xkh?WF*|k%>U0uL+MVKwbk~%vk`a>iW_dsw58qHi`*x zeim>Dm}^S2L3Q*Y$vRS|k;Fu{x0i{-%a2c5L7`P<5VioYc1+)j=n~~_9 zfy*Xr073v7XbXM=w6lOJ0O?FH4GN%- z=vxkKwg3qN+UWZM1MsuHYOW$unJ@&nW(6mKsrr@zA6@}H^<_pGW{3%mfCwvC0PNFu z9hkTWSftPAZv{BAnNb$ta|`GWJlFROXgwJ?txxV5wul*@8_HV1Pk>6U?J_W@6%b;= zUVsXwNCeQ(0x|)V)HWN4=?n0cw!rtmBQfV1FvJ3E5kM1dr4`^bRMXZ5AjqW6z;X+a zi@;cIvL3e`b=mF!N~JMp0&vO-Rsh@erM&9qUSoefzhIz*IqiTqR?rW4pife+Q7+(B zzce}y0ZLKFoL>Q_75oV}66q5uw%rv(%*qk3!pZ>s&1nmSTR|>>f@Wkq;|`qLs@XhS z1}FuKOi6whIAI08F+e>%DN8gMzV2!qz@_UhfLk4sdypX(Z~{loh`9sM-U{RzFfl$~ zR_!qB@Ebe~$|6;FI$c2M7T%C@usMO3xtq+Fa3u{NUsGPwr@ys*-r3o+R=NIp5 zPvIw@wqFXnc%Cx3=ueVHcM|ze^1u`+Ql#+cn+zWLCl#@$@F;PP6bY8SK-`z^3SEc0 zr97AzD)JY3+}TD(;aC=!KuMdiQtW7Po8;y3Z!(^IpWNN#aFOcdb=O`sN%}|Zlmv>{ z4&MxL_Z#1fRHM2F(Ty8V_vFk)5!v)6q0%|7q_}TLagjna^T2Wa`cqGC?-P-E(Fv3s zkz>TeylRL@Z&kp9aFObstO!Xb{`Zb2OzfxB`sK?4+{iq~x)p!V6>T{`JeqkiqD;ah zaMbA0qeuC>v(=nCr@DCf!$6VTqtm*RuZAognN9v5HC9kKAh-$u0MIu8odGJy080Qq zQ6!ETnU>*MubZyxYJ+xr0g z(f!SI6AN4mf6{S!R~{=MK0^xyTqz#qoHvwx=hyZCDVZ~RB)=d|C-zq3h=ermS-Gg9F@t*U}t? zj$U7bmV>Um(lxrsEB6zMX`)-Rq}pfPTAA-q0RH{JjQ=A6Sx0{e1jk#}$4rSvromsN z`C;R=UL^`QWM6cx6@6cPxRkETcz}@V*Eq!m1DBLkyN8SuX>Lc~tMs z6m~~|ifEe&4%WIWDnU6K?E^1-*M)wUDiL!uz=vWxS(y(7BD~o55-2qWEgU(y7O!h4=CJrBEzWb<1_yjg9|X0ILoTo_bXf3d>Ly*NzC;jmqrl)HJw{Pq(d z?OQoO*YsX^7$>p4hpwJ*z1&==d6tN?SQXybF0$Md4E`9iIfXD$eUh+2*$nB*@>viq zx*;*vDW&#Za2uPb!*T69u1)I_Q+X-%V7TEl5TiXZs78$uW4<$fWS z2-0+3sv*vOvv4V_Fn!r{&NV8jp%4aiG>jX(^wj4h4QY9xM4Ggw#!9*0g^{JJ(sTVY zUR%6`MV*^IV*ga1$B!jwP3~E#9p><%SCW4(zW*Ht9Jy8_-=jXrw0}<18*(R;Vdvwswa7!30=}i*Lso**y<~AhlS5t*9LRKjN z4=X-5?!3Li)&3spR5DL7SG1;NEB2m|ERgQN{Z9`i{&JilU)yh~qOo|Fr+%2p@1cCT z#fZf{WNPl&pbYOU9+0ax;+K5T%vLzYb4VV0G(YjH9gN4@K2wkVV7rjHmr8X9bBff` zNj$PZbxVKQvAG%2UAhkfO4dGs5B;EfPx#I6Ykdb`zrxsfkzW}<8(mR()r(}jfYLnY zY-vi4@^?7r>|s0r+Tx%DuPLlMamgZ!^$5gjAyer?+Mh9u2CLm68~wUs5;_2XaQQ1 zb+B|uT`LyN()Q=~_voyK3V-O;bR}zqyWA*V^K$U9A}5kA%pCF_@212cb0*f!b=`*a z%8U{JsnX+1eF-l;J}JkrScRh2sD%qF3D+_+I>XHE^X)#T4`VBC-H!N}cBR7_c_f$; z@fRog?ex%}t6z7>VR8A{{VCsFNRmZ|_+FQaTDrsn@p#Y93Pc^kg8kL`)-?@Lx&o@t zaLw)OgS8&zm-$Z;qvQX}5)pxR<(3j@Urd}G&0z)aO^S`@3j4{Ywn)e&OT=ehJGYT zv(-ytUE4EPbQE4i*)SDwT(_XTB(<;yGaIZl1l;0`XLY=`08qWUMLc-UL0|XKMoG1m zSOWwqENaSJgPC)bobuttE~!5_Fxl#TfE!H8lJzusaz|Ja9d>~eQ4~76t4!j^R%L+F z@KZSh8RwH8X;uB^|hI*Lvl&eF59H+#`vA)O&)16d<7>u>1%z&6z%l+8EqMD*w$(0NN>dtrtR$K3WcR`ANH0bi~3Nurr*JXKBMvg$oF%3rBH}qbQ^nR znoL#h^}(+-3@w9E;&#yTI%cas-eLLi*D3$Wx5=IQ`E>zg0&xi-Bw=e>Iz$^N8|6oa#CE*zg_oA;XLHltwtmzo4GLR;vRUVJ;U%cEI5C?1whIQ zUQBEu*fx|^HZw0cGZHc0uiq*k7JoSZcmZe}3C-41Fl` z3W(t7u?`Qe;2C`>5F1YDsG(SCftjvYk;oWCKXfWcI5GER@I^`H4+3i0 zx-*Z`Yr80!MJo-o3K)aF?6Rr(G}~R;f8VmT8rZ5!B(a;L>pR(&dUoA@NKa&Ny0h1v zB>u=?HV6@-F4NA6tq+;_MQ2e@;-B=QQu3{nbV%UGY6ilL2ULLvz3B8-YWj**V|X;V z4f%>8+JB+d@of#i;mT3}B-~rzvOfdq&3-a(XJI5-6pUX2GG4MGMPsdk_zyJUM0MjI{&;S4c diff --git a/test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp b/test/samples/02_sample_invoice.pdf_images/page_undefined_img_2.webp deleted file mode 100644 index c0352072c6c2627dae4f5ae199ffe13a11980af2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 736 zcmV<60w4WSNk&H40ssJ4MM6+kP&il$0000G0000J0RS%m06|PpNLl~@00Cd1rfnlQ z*i4;^;E25u5h~1Mnt)Ah?M-MdkkI`gp^M0rDSJ$?!tXHe8z~X}pMWe~cNx=HncUH^ zwVs1B=?R>u=j7P$zsIV;iFyvstY_pjdNOxIe3cGXP&go10ssI|5dfV5Dii@Q06tM7 zkVYe+p`jxaI`D7`326Wo#ItVyb_Rg*!~kl31d9KH8DTJ?H<(ZT%G*7KE%?@dXrU2GZ!;>J;B4>Eo^n@{|L z`J%rT-OLG~4oHC}9_+CK63Dx6v3lwb*#M_$`hWob{dRdF`w7cYp+(2;m}%C4x1~sF zn~zIO+8>@9%DzLdaS~&VHL>|hs(<_EORRDb4VN-NUheHYkUfkXv%q6l*L53rRVLXH zA7mf4G&hx%31HfRJLF1}7b>v(2_SFxqJ!Pe@KU|^({jt5-*h`3^PoKdz@VU>dviXx z-^JVgnSxSZe{)+Iu!8=mOj4~B@AF*kD(`wD_$^)jJAd^xrC-*|h7M5%mbQQ5m0*?A>ppnyE0I3GS?jfI2oNb_NIM(R?fkN7hUa8^ z<^5hXZU1EnTWeFKX`5~CpP%aN*-vZE5veOzDkb%C|I5kf4q<+3ZnB{ks`IEd5K2FT zpSjxtV}Gkw4!tEFg2bW)MQ(Ti4l;TDNKsNX7cwz!!>{pGzHGc}1B_~G0CgDR39UY| z_ddaL5}tE`hGAi}V4nl~y;87yYI`D*i+W*2OVT#Uqx&^8l{uw_CM{J@C8*Jc|Nk#c zIGtTi_s{nr!VoM;))0TEgukjQ-SjzVq6_|e#dewW)X)g$*bJ9PChzl~>+^9~5jAW8 SzEUY+Mn}Vap2b*HKmY(a+ghan diff --git a/test/samples/03_sample_compex.pdf.md b/test/samples/03_sample_compex.pdf.md deleted file mode 100644 index 927b233a..00000000 --- a/test/samples/03_sample_compex.pdf.md +++ /dev/null @@ -1,2142 +0,0 @@ -# arXiv:2302.11942v3 [q-fin.MF] 10 Mar 2023 - -## Liquidity Providers Greeks and Impermanent Gain - -### Niccolò Bardoscia, Alessandro Nodari ∗ - -### 23 February 2023 - -## Abstract - - In traditional finance, the Black & Scholes model has guided almost 50 years of derivatives pricing, defining a standard to model any volatility-based product. With the rise of Decentralized Finance (DeFi) and constant product Automated Market Makers (AMMs), Liquidity Providers (LPs) are playing an increasingly important role in markets functioning, but, as the recent bear market highlighted, they are exposed to important risks such as Impermanent Loss (IL). In this paper, we tailor the formulas introduced by Black & Scholes to DeFi, proposing a method to calculate the greeks of an LP. We also introduce Impermanent Gain, a product that LPs can use to hedge their position and traders can use to bet on a rise in volatility and benefit from large market moves. - - Keywords: liquidity providers, impermanent loss, constant product automated market makers, impermanent gain - - ∗The authors are greteful to 0xSami_, Andrea Bugin, Andrea Prampolini, Barry Fried, Carlo Sala, Fabio Bellini, Giulio Anselmi and Miguel Ottina for providing valuable feedback on drafts of the article. - - -## 1 Introduction - -Before diving deeper on the subjects of this article we will present, in this first section, a brief review of the ecosystem in which we will operate and of its principal actors. - -### 1.1 Decentralized Exchanges - -A Decentralized Exchange (DEX) is a peer-to-peer marketplace where transactions occur directly between crypto traders. DEXs fulfill one of crypto’s core possibilities: fostering financial transactions that aren’t officiated by banks, brokers, payment processors, or any other kind of intermediary. The most popular DEXs, like Uniswap and Sushiswap, utilize the Ethereum blockchain and are part of the growing suite of DeFi tools which make a huge range of financial services available directly from a compatible crypto wallet. Unlike Centralized Exchanges (CEXs) like Binance and Coinbase, DEXs don’t allow for exchanges between fiat and crypto. All the transactions in a CEX are handled by the exchange itself via an order book that establishes the price for a particular cryptocurrency based on current buy and sell orders. DEXs, on the other hand, are simply a set of smart contracts: they establish the prices of various cryptocurrencies against each other algorithmically and use Liquidity Pools, in which investors lock funds in exchange for a percentage of the trading fees, in order to facilitate trades. While transactions on a CEX are recorded on that exchange’s internal database, DEX transactions are settled directly on the blockchain. DEXs are usually built on open-source code and developers can adapt existing code to create new competing projects. - -### 1.2 Liquidity Provider - -A Liquidity Provider in crypto, from here on LP, is an investor (individual or institution) who, as the name suggests, funds a liquidity pool with crypto assets it owns in order to facilitate trading on the platform and earn passive income on its deposit. The more assets in a pool, the more liquidity the pool has, and the easier trading becomes on a DEX for other market participants hence the crucial role played by LPs. How much the DEX pays the LP is based on the percentage of the crypto liquidity pool it puts, the volume, and the transaction fee offered by the exchange to LPs. - -1.2.1 Liquidity Provider Tokens - -Liquidity Provider tokens (LP tokens) are crypto tokens given to users who deposit their crypto into a Liquidity Pool. LP tokens represent the Liquidity Provider’s share of the pool and can be redeemed at any time for the underlying assets. Some platforms require LP tokens to be locked for a period of time in order to access additional rewards (Liquidity Mining). - -### 1.3 Constant product AMM - -Here we will present in brief what is a constant product AMM for a more in depht analysis of AMM see for example [1], [2], [3] and [4]. A constant product AMM is a type of AMM in which the reserves of tokens are regulated by a product function, given two tokens x and y we have that: - - x · y = k - -Where x and y represent, with an abuse of notation, the quantities of token x and y respectively and k is the constant. For example in Uniswap, see [5] and [6], they call that constant L^2 so that: - - √ x · y = L - -We will indicate with St the pool price of token x in terms of token y at time t. In this type of AMM we can derive the price as the ratio of the number of token y and of token x at time t: - - St = - - yt xt - -Another interesting feature of this type of pools is that knowing in an instant t 0 the following data: - -- the constant of the pool L; - - -- the number of token x at time t 0 given by x 0 ; - -- the number of token y at time t 0 given by y 0 ; - -and supposing that there is no injection of new capital in the pool till time T , we can calculate the number of tokens in every time t ∈ [t 0 , T ] as: - - xt = x 0 - -#### √ - -#### S 0 - - St - - yt = y 0 - -#### √ - - St S 0 - -#### (1. 3 .1) - -Having established these properties we can start analyzing how the position of a LP evolves in the pool. At time t 0 , that we will assume without losing of generality that is equal to 0, he deposits a certain quantity of token x and y given by: - - x 0 = - -#### L - -#### √ - -#### S 0 - - y 0 = L - -#### √ - -#### S 0 - -Doing so we have that the constant product is respected. So we can calculate the initial capital invested, in terms of token y, as: - - V 0 = x 0 S 0 + y 0 = 2L - -#### √ - -#### S 0 - -When the price moves the quantity of each token changes according to (1.3.1) so that we have: - - xt = - -#### L - -#### √ - - St - - yt = L - -#### √ - - St - -And so it also changes the value of the position: - - VLP (t) = xtSt + yt = 2L - -#### √ - - St = V 0 - -#### √ - - St S 0 - -#### = V 0 - -#### √ - - αt - -Where we have defined αt as the ratio of the change in price. So the P&L of the LP position at time t is: LP (t) = Vt − V 0 = V 0 ( - -#### √ - - αt − 1) - -What we have ignored till now are the fees that Traders have to pay when they do their swaps. Part of these fees go to the protocol while the remainder is divided between the LPs proportional to the quantity of liquidity they have provided. The fees can be expressed as: - - Φ(t) = V 0 · φ · t - -Where φ is the expected APY on the specific Liquidity Pool so that the actual payoff should be: - - VLP (t) = V 0 - -#### √ - - αt + Φ(t) = V 0 ( - -#### √ - - αt + φt) (1. 3 .2) - -While the final P&L is: LP (t) = V 0 ( - -#### √ - - αt + φt − 1) - -### 1.4 HODLer - -Before moving on we introduce one last character: the HODLer. In DeFi jargon an HODLer indicates a user that holds its tokens without doing anything. That is to say that given an initial quantity of tokens: - - x 0 and y 0 - -at every time t the HODLer will have: - - xt = x 0 and yt = y 0 - -Thus the position value of an HODLer is determined just by the change in price of the tokens. The position value of an HODLer expressed in terms of token y is given by: - - VH (t) = x 0 St + y 0 - - -## 2 Impermanent Loss - -Impermanent Loss (IL) is a known problem affecting the LPs defined as the difference between the return of a LP and that of an equal-weight (with respect to the starting time) HODLer. For a more in depth analysis of it on UniSwap, the most used constant product AMM, see [7] and [8]. We recall the starting quantities of tokens for a LP and hence of an equal-weight HODLer: - - x 0 = - -#### L - -#### √ - -#### S 0 - - y 0 = L - -#### √ - -#### S 0 - -and the values of a LP position and that of an HODLer at time t: - - VLP (t) = xtSt + yt = - - LSt √ St - -#### + L - -#### √ - - St = V 0 - -#### √ - - αt - - VH (t) = x 0 St + y 0 = - - LSt √ S 0 - -#### + L - -#### √ - -#### S 0 = V 0 - -#### ( - - αt + 1 2 - -#### ) - -Now we can compute the IL: - - IL(t) = VLP (t) − V 0 V 0 - -#### − - - VH (t) − V 0 V 0 - -#### = - - VLP (t) − VH (t) V 0 - -#### = - -#### √ - - αt − αt 2 - -#### − - -#### 1 - -#### 2 - -#### (2.1) - -Formula (2) is the same found in [8]. We can also express that in terms of the token return defining: - - rt := - - St S 0 - - − 1 = αt − 1 - -Doing so we obtain the following equivalent form: - - IL(r) = - -#### √ - - r + 1 − - - r 2 - -#### − 1 (2.2) - --1.2 -1 (^0 1) Percentage Return 2 3 4 5 -1 -0.8 -0.6 -0.4 -0.2 0 Percentage Loss **Impermanent Loss** We plotted the IL given by formula (2.2). We note that IL(r) ≤ 0 and equal to zero only if the price of token x is exactly the same as its starting price S 0 (r = 0). This tells us that being a LP is always worse than being an HODLer, unless the fees are enough to offset this difference. - -### 2.1 LP as an option seller - -It has been noted, for example in [9], that being a LP, if we consider the IL, is actually the same as being an option seller. In fact we know that we can replicate any given twice differentiable payoff h(x) via the formula: - - - h(ST ) = h(S 0 ) + h′(S 0 )(ST − S 0 ) + - -∫ (^) S 0 0 h′′(K)(K − ST )+dK + ∫ (^) ∞ S 0 h′′(K)(ST − K)+dK In our case we have that our payoff function h is the IL that is: h(x) = - -#### √ - - x S 0 - -#### − - - x 2 S 0 - -#### − - -#### 1 - -#### 2 - -clearly this function is C∞(R+) so that we can apply the replication formula. First we will compute the first and second derivative of the function:      - - h′(x) = - -#### 1 - -#### 2 - -#### √ - - xS 0 - -#### − - -#### 1 - -#### 2 S 0 - - h′′(x) = − - -#### 1 - - 4 x^3 /^2 - -#### √ - -#### S 0 - -#### < 0 - -Thus substituting we obtain: - - h(ST ) = − - -∫ (^) S 0 0 - -#### 1 - -#### 4 K^3 /^2 - -#### √ - -#### S 0 - - (K − ST )+dK − - -∫ (^) ∞ S 0 - -#### 1 - -#### 4 K^3 /^2 - -#### √ - -#### S 0 - - (ST − K)+dK - -That is to say that we can replicate the IL selling an infinite strip of puts and calls of all strikes with maturity T. - - -## 3 LP pricing and Greeks - -On most DEXs like Uniswap, LPs can withdraw their liquidity at any time by redeeming their LP tokens. In this case, the price of the LP position is, as seen in formula (1.3.2), always equal to the value of the underlying assets plus fees: - - Pt = V 0 ( - -#### √ - - rt + 1 + φt) = V 0 - -#### (√^ - - St S 0 - - + φt - -#### ) - -Where rt is the return of asset x relative to asset y and φ is the expected APY. - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 5000 10000 15000 Pt **LP Position Price** As we will see below, unlocked liquidity has a positive delta, negative gamma and positive theta, while the vega exposure is zero. This is particularly important when options are used to hedge the position as suggested in [5] and [6]. Options do have a vega exposure, therefore an unlocked LP portfolio combined with a long options position always has a positive vega. - -### 3.1 Unlocked Liquidity Greeks - -3.1.1 Delta - -Delta is defined as the partial derivative of the price of the position (Pt) with respect to the underlying price (St): - -#### ∆LP := - - ∂Pt ∂St - -#### = - -#### V 0 - -#### 2 - -#### √ - - S 0 St - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 20 40 60 80 100 120 140 160 LP **Delta LP** - - -3.1.2 Delta 1% - -Delta 1% is defined as the change in price of the position when the underlying price changes by 1%. So we will compute it as: - - ∆1% LP (St) := ∆LP (St) · - - St 100 - -#### = - -#### V 0 - -#### 2 - -#### √ - - St S 0 - -#### · 10 −^2 - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 10 20 30 40 50 60 70 80 1% LP **Delta 1% LP** 3.1.3 Gamma Gamma is defined as the second partial derivative of the price of the position (Pt) with respect to the underlying price (St). It can also be seen as the partial derivative of Delta with respect to the underlying price: - -#### ΓLP := - - ∂^2 Pt ∂S t^2 - -#### = - -#### ∂∆LP - - ∂St - -#### = − - -#### V 0 - -#### 4 - -#### √ - -#### S 0 S - - 3 / 2 t - -We note that ΓLP < 0 and that ΓLP → −∞ when St → 0. - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -2.5 -2 -1.5 -1 -0.5 0 LP **Gamma LP** - - -3.1.4 Gamma 1% - -Gamma 1% is defined as the change of the Delta 1% when the underlying price changes by 1%. So we will compute it as: - - Γ1% LP (St) = ΓLP (St) · - -#### ( - - St 100 - -#### ) 2 - -#### = − - -#### V 0 - -#### 4 - -#### √ - - St S 0 - -#### · 10 −^4 - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -0.4 -0.35 -0.3 -0.25 -0.2 -0.15 -0.1 -0.05 0 1% LP **Gamma 1% LP** 3.1.5 Vega Vega is defined as the partial derivative of the price of the position with respect to the volatility (σ): νLP := ∂Pt ∂σ - -#### = 0 - -3.1.6 Theta - -Theta is defined as the partial derivative of the price of the position (Pt) with respect to time (t): - -#### ΘLP := - - ∂Pt ∂t - - = φ · V 0 - -3.1.7 Rho - -Rho is defined as the partial derivative of the price of the position (Pt) with respect to the risk-free rate (rf ): - - ρLP := - - ∂Pt ∂rf - -#### = 0 - -### 3.2 Locked Liquidity Analysis - -From here onwards, we assume that the Liquidity Provider has locked his liquidity until time T , perhaps to access liquidity mining rewards. When he does so, he will no longer be able to redeem the underlying assets until maturity. In this case, the fair price of his position may differ from the value of the underlying assets. As we will see, while unlocked liquidity gives exposure only to delta, gamma and theta, locking the liquidity exposes the holder to additional vega and rho risks. In order to calculate the fair price of the locked LP position we need to know: - -- the maturity T when the liquidity gets unlocked (in years); - -- the current time t (in years); - - -- the remaining time τ = T − t (in years); - -- the starting price for the underlying S 0 ; - -- the initial capital invested V 0. - -Similar to Black & Scholes, we assume that the price processes are governed by the following SDEs: - - dBtx = rxBtx dt dByt = ry Bty dt dSt = μStdt + σStdWt - -Where Bxt is the "bond" process relative to token x with risk free rate rx (we can take for example the APY on lending token x on Aave), Byt is the "bond" process relative to token y with risk free rate ry (analogously we can take the APY on lending token y on Aave), St is the market price of token x in terms of token y determined by the drift μ, the volatility σ and the Brownian Motion (BM) Wt. So this is to say that the price process is a Geometric Brownian Motion (GBM). It is known that we can find a probability Q (risk-free probability) in which we have that the price process follows the following SDE: - - dSt = (rx − ry )Stdt + σd W˜t - -Where we have that W˜t is another BM given by the Ito formula: - - d W˜t = dWt − - - μ − (rx − ry ) σ - - dt - -We can solve the price process SDE and defining rf := rx − ry we obtain: - - St = S 0 exp - -#### { - - rf t − - -#### 1 - -#### 2 - - σ^2 t + σ W˜t - -#### } - -We will assume that the liquidity pools are efficient, thanks to the presence of arbitrageurs, so that the pool price of token x in terms of token y is arbitrarily close to the market price, except at most in some instants, so that we can use the market price dynamics also for the pool price dynamics. From here onward we will denote everything in terms of token y. Now we can find the price of a locked liquidity position computing the discounted payoff under Q: - - Pt = EQ[e−rf^ τ^ VLP (T )] = e−rf^ τ^ EQ - -#### [ - -#### V 0 - -#### √ - -#### S 0 - -#### √ - -#### ST + Φ(T ) - -#### ] - - = e−rf^ τ - -#### ( - -#### V 0 - -#### √ - -#### S 0 - -#### EQ[ - -#### √ - -#### ST ] + Φ(T ) - -#### ) - -Thanks to the property of the GBM we have that: - -#### EQ[ - -#### √ - -#### ST ] = - -#### √ - - St exp - -#### { - -#### 1 - -#### 2 - - rf τ − - -#### 1 - -#### 8 - - σ^2 τ - -#### } - -#### (⋆) - -See Appendix A for the full derivation of (⋆). Substituting this into the pricing formula and doing some rearrangements we obtain: - - Pt = V 0 - -#### (√^ - - St S 0 - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - - + φT e−rf^ τ - -#### ) - -This is the fair value of the LP position locked until time T , when the expected volatility on the pair of tokens is σ and φ is the expected APY of the pool. - - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - 0 - - 5000 - - 10000 - - 15000 - - Pt - - LP Position Price - -Plot obtained with the following data: - -- V 0 = 10000, S 0 = 1000; - -- rf = 3%, σ = 70%, φ = 10%; - -- T = 0. 5 , τ = 0. 25. - -All the plots in this section are obtained using the same data. - -### 3.3 Greeks - -3.3.1 Delta - -#### ∆LP = - -#### V 0 - -#### 2 - -#### √ - - S 0 St - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - -We note that ∆LP > 0. - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 20 40 60 80 100 120 140 160 LP **Delta LP** We can see that the delta of a liquidity provider increases when the underlying price S drops and vice versa. - - -3.3.2 Delta 1% - - ∆1% LP (St) = - -#### V 0 - -#### 2 - -#### √ - - St S 0 exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - -#### · 10 −^2 - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 10 20 30 40 50 60 70 1% LP **Delta 1% LP** 3.3.3 Gamma - -#### ΓLP = − - -#### V 0 - -#### 4 - -#### √ - -#### S 0 S - - 3 / 2 t - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - -We note that ΓLP < 0 and that ΓLP → −∞ when St → 0. - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -2.5 -2 -1.5 -1 -0.5 0 LP **Gamma LP** 3.3.4 Gamma 1% Γ1% LP (St) = − - -#### V 0 - -#### 4 - -#### √ - - St S 0 - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - -#### · 10 −^4 - - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -0.35 - - -0.3 - - -0.25 - - -0.2 - - -0.15 - - -0.1 - - -0.05 - - 0 - - 1% LP - - Gamma 1% LP - -3.3.5 Vega - - νLP = −V 0 - - στ 4 - -#### √ - - St S 0 - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - -We note that νLP < 0. - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -7 - - -6 - - -5 - - -4 - - -3 - - -2 - - -1 - - 0 - - LP - - Vega LP - -In the figure we have plotted the Vega corresponding to a 1% change in volatility σ, that is ν/ 100. - -3.3.6 Theta - -#### ΘLP = V 0 - -#### (√^ - - St S 0 - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - - + rf φT e−rf^ τ - -#### ) - - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - 0 - - 0.5 - - 1 - - 1.5 - - 2 - - 2.5 - - 3 - - LP - - Theta LP - -In the figure we have plotted the daily Theta, that is Θ/ 365. - -3.3.7 Rho - -Even if we have a model with two risk free rates we note that the price depends only on their difference rf so we can study only the sensibility with respect to rf. - - ρLP := ∂Pt ∂rf - -#### = −V 0 - -#### ( - - τ 2 - -#### √ - - St S 0 - - exp - -#### { - - − τ - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### )} - - + τ φT e−rf^ τ - -#### ) - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -20 - - -18 - - -16 - - -14 - - -12 - - -10 - - -8 - - -6 - - -4 - - -2 - - 0 - - LP - - Rho LP - -In the figure we have plotted the Rho corresponding to a 1% change of rf , that is ρ/ 100. - - -## 4 Impermanent Gain - -As detailed in the previous section, a LP is exposed to many risks. While it is relatively easy to hedge the Delta, for example shorting futures on the underlying S, we ask ourselves if we can structure a product that hedges all the other greeks, in particular Vega, Gamma and Theta. We call this product Impermanent Gain (IG). IG’s unit payoff is defined as the opposite of IL: - - IG(r) = - -#### VH − VLP - -#### V 0 - -#### = 1 + - - r 2 - -#### − - -#### √ - - r + 1 - -The IG is a product that has some similarities with European options: - -- it has a maturity T ; - -- it has a strike K that is the starting price from which the IG is computed: - - rt = - - St K - -#### − 1 - - where rt is used to compute the IG at time t. - -IG payoff with maturity T can be coded into a smart contract making it a DeFi-native product. - -### 4.1 Pricing - -We have seen in Section 2.1 that we can replicate the IL selling an infinite strip of puts and calls and so we can also replicate the IG buying the same portfolio, thus we could price the IG position finding the cost of the replicating portfolio. Instead of doing that we will use a different approach giving a dynamic for the price process of token x, in terms of token y, and using as the price for the IG position the discounted payoff. We will assume the same dynamics and conditions described in Section 3.2 for the market and liquidity pool price processes. Given the following data: - -- maturity T (in years); - -- current time t (in years); - -- time to expiry τ = T − t (in years); - -- strike K = S 0 , that is the starting price of the token; - -- initial capital V 0 ; - -we can calculate the price of the IG strategy at time t as the discounted payoff under the risk-free measure Q: - - Pt = EQ[e−rf^ τ^ · V 0 · IG(ST )] = EQ - -#### [ - - e−rf^ τ^ V 0 - -#### ( - -#### 1 - -#### 2 - -#### + - -#### ST - -#### 2 K - -#### − - -#### √ - -#### ST - -#### K - -#### )] - - Pt = e−rf^ τ^ V 0 - -#### ( - -#### 1 - -#### 2 - -#### + - -#### 1 - -#### 2 K - -#### EQ[ST ] − - -#### 1 - -#### √ - -#### K - -#### EQ[ - -#### √ - -#### ST ] - -#### ) - -Remembering the properties of the GBM we have: - - EQ[ST ] = Sterf^ τ^ (•) - -#### EQ[ - -#### √ - -#### ST ] = - -#### √ - - St exp - -#### { - -#### 1 - -#### 2 - - rf τ − - -#### 1 - -#### 8 - - σ^2 τ - -#### } - -See Appendix B for the full derivation of (•). Substituting into the price formula and doing some rearrangements we obtain: - - Pt = V 0 - -#### ( - -#### 1 - -#### 2 - - e−rf^ τ^ + - - St 2 K - -#### − - -#### √ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### }) - - - 0 500 1000 1500 2000 2500 3000 3500 4000 St - - 0 - - 1000 - - 2000 - - 3000 - - 4000 - - 5000 - - 6000 - - Pt - - IG Position Price - -Plot obtained with the following data: - -- V 0 = 10000, K = 1000; - -- rf = 3%, σ = 70%; - -- τ = 3657 (seven days). - -All the plots in this section are obtained using the same data. - -### 4.2 Greeks - -4.2.1 Delta - -#### ∆IG = V 0 - -#### ( - -#### 1 - -#### 2 K - -#### − - -#### 1 - -#### 2 - -#### √ - - KSt - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### }) - -We note that the ∆IG(K) > 0. - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -160 - - -140 - - -120 - - -100 - - -80 - - -60 - - -40 - - -20 - - 0 - - 20 - - IG - - Delta IG - - -4.2.2 Delta 1% - - ∆1% IG (St) = V 0 - -#### ( - - St 2 K - -#### − - -#### 1 - -#### 2 - -#### √ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### }) - -#### · 10 −^2 - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -15 - - -10 - - -5 - - 0 - - 5 - - 10 - - 15 - - 20 - - 25 - - 30 - - 1% IG - - Delta 1% IG - -4.2.3 Gamma - -#### ΓIG = - - ∂^2 Pt ∂S^2 t - -#### = - -#### V 0 - -#### 4 - -#### √ - - KS t^3 /^2 - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - -We note that ΓIG > 0 and that ΓLP → +∞ when St → 0. - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 0.5 1 1.5 2 2.5 IG **Gamma IG** 4.2.4 Gamma 1% Γ1% IG (St) = - -#### V 0 - -#### 4 - -#### √ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - -#### · 10 −^4 - - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - 0 - - 0.05 - - 0.1 - - 0.15 - - 0.2 - - 0.25 - - 0.3 - - 0.35 - - 0.4 - - 1% IG - - Gamma 1% IG - -4.2.5 Vega - - νIG = V 0 - - στ 4 - -#### √ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t 0 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 IG **Vega IG** In the figure we have plotted the Vega corresponding to a 1% change in volatility σ, that is ν/ 100. 4.2.6 Theta - -#### ΘIG = V 0 - -#### ( - - rf 2 - - e−rf^ τ^ − - -#### √ - - St K - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### }) - - - 0 200 400 600 800 1000 1200 1400 1600 1800 2000 St - - -3 - - -2.5 - - -2 - - -1.5 - - -1 - - -0.5 - - 0 - - 0.5 - - IG - - Theta IG - -In the figure we have plotted the daily Theta, that is Θ/ 365. - -4.2.7 Rho - -ρIG = − - - V 0 τ 2 - - e−rf^ τ^ + - - V 0 τ 2 - -#### √ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - -#### = - - V 0 τ 2 - -#### (√^ - - St K - - exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - - − e−rf^ τ - -#### ) - -(^0 200 400 600 800 1000) S 1200 1400 1600 1800 2000 t -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 IG **Rho IG** In the figure we have plotted the Rho corresponding to a 1% change of rx, that is ρ/ 100. - -### 4.3 Impermanent Gain as a Hedging Tool - -Imagine an LP provides $ 10000 of full-range liquidity on Uniswap on the ETH/USDC pair. He then locks his liquidity for 1 year using a liquidity mining platform. ETH price is $ 1000. He wants to hedge his position using Impermanent Gain. We have: - -- V 0 = 10000, S 0 = 1000; - -- rf = 3%, σ = 70%, φ = 10%; - - -- T = 1, τ = 1. - -For the IG, the LP chooses a strike K equal to S 0 = 1000 and a maturity of 1 year. We can compute the greeks of this position. - -4.3.1 Delta - -#### ∆ = ∆IG + ∆LP = - -#### V 0 - -#### 2 K - -We note that the Delta is a constant determined by the invested capital V 0 and the strike K. In our case we have ∆ = 5. - -4.3.2 Gamma - - Γ = ΓIG + ΓLP = 0 - -Doing this hedge the resulting position is a Gamma-neutral one. - -4.3.3 Vega - - ν = νIG + νLP = 0 - -Doing this hedge the resulting position is a Vega-neutral one. - -4.3.4 Theta - - ΘIG + ΘLP = V 0 rf - -#### ( - -#### 1 - -#### 2 - - + φT - -#### ) - - e−rf^ τ - -We note that now Theta doesn’t depend on the underlying price St and on the volatility σ. - -4.3.5 Rho - - ρ = ρIG + ρLP = −V 0 τ - -#### ( - -#### 1 - -#### 2 - - + φT - -#### ) - - e−rf^ τ - -We note that now Rho doesn’t depend on the underlying price St and on the volatility σ. - -As we have seen, for a liquidity provider with locked liquidity, buying Impermanent Gain completely eliminates Gamma and Vega risks as well as significantly reduces Theta and Rho. - -## 5 Conclusion - -In this paper we analyzed the risks of a liquidity provider with a focus on Impermanent Loss, detailing the position greeks under Black & Scholes assumptions. We found that a liquidity provider has a positive Delta, negative Gamma and positive Theta, while the Vega is zero; we also demonstrated that locking the liquidity changes the risk profile of a liquidity provider and introduces a negative Vega risk. Additionally, we introduced Impermanent Gain, a DeFi-native product tailored for liquidity providers’ needs and demonstrated how it can be used to eliminate most financial risks related to providing liquidity. - - -## Appendix A - -Recall that: - - ST = St exp - -#### {( - - rf − - - σ^2 2 - -#### ) - - τ + σWτ - -#### } - - Wτ ∼ N (0, τ ) - -Recall also that given a standard normal distribution Z ∼ N (0, 1) we have that: - -#### E - -#### [ - - exp{uZ} - -#### ] - - = exp - -#### { - - u^2 2 - -#### } - -Now we can proceed: - -#### E[ - -#### √ - -#### ST ] = E - -#### [√ - - St exp - -#### { - -#### 1 - -#### 2 - -#### ( - - rf − σ^2 2 - -#### ) - - τ + σ 2 - - Wτ - -#### }] - -#### = - -#### √ - - St exp - -#### { - -#### 1 - -#### 2 - -#### ( - - rf − σ^2 2 - -#### ) - - τ - -#### } - -#### E - -#### [ - - exp - -#### { - - σ 2 - -#### √ - - τ Z - -#### }] - -#### =⇒ E[ - -#### √ - -#### ST ] = - -#### √ - - St exp - -#### { - - rf 2 - - τ − - - σ^2 4 - - τ - -#### } - - exp - -#### { - - σ^2 8 - - τ - -#### } - -#### = - -#### √ - - St exp - -#### {( - - rf 2 - -#### − - - σ^2 8 - -#### ) - - τ - -#### } - -## Appendix B - -Recall that: - - ST = St exp - -#### {( - - rf − - - σ^2 2 - -#### ) - - τ + σWτ - -#### } - - Wτ ∼ N (0, τ ) - -Recall also that given a standard normal distribution Z ∼ N (0, 1) we have that: - -#### E - -#### [ - - exp{uZ} - -#### ] - - = exp - -#### { - - u^2 2 - -#### } - -Now we can proceed: - -#### E[ST ] = E - -#### [ - - St exp - -#### {( - - rf − σ^2 2 - -#### ) - - τ + σWτ - -#### }] - - = St exp - -#### {( - - rf − σ^2 2 - -#### ) - - τ - -#### } - -#### E - -#### [ - - exp - -#### { - - σ - -#### √ - - τ Z - -#### }] - - =⇒ E[ST ] = St exp - -#### { - - rf τ − - - σ^2 2 - - τ - -#### } - - exp - -#### { - - σ^2 2 - - τ - -#### } - - = Sterf^ τ - - -## Appendix C - -The following table is a Greeks comparison between the different strategies. Let’s first define: - - β := exp - -#### { - -#### − - -#### ( - - rf 2 - -#### + - - σ^2 8 - -#### ) - - τ - -#### } - - γ := e−rf^ τ - - Greeks Unlocked LP Locked LP Impermanent Gain - - ∆ 2 √VS^00 St 2 √^ VS^00 St β V 0 - -#### ( - - 1 2 K −^ - - β 2 √KSt - -#### ) - -#### ∆1%^ V 20 - -#### √ - - St S 0 ·^10 - - − 2 V 0 2 - -#### √ - - St S 0 β^ ·^10 - -− (^2) V 0 - -#### ( - - St 2 K −^ - - 1 2 - -#### √ - - St K β - -#### ) - -#### · 10 −^2 - -#### Γ − V^0 - - 4 √ S 0 S^3 t/^2 - -#### − V^0 - - 4 √ KS t^3 /^2 β ∂ - -(^2) Pt ∂S^2 t^ =^ V 0 4 √ KS^3 t/^2 β Γ1%^ − V 40 - -#### √ - - St S 0 ·^10 - -− (^4) − V 0 4 - -#### √ - - St S 0 β^ ·^10 - - − 4 V 0 4 - -#### √ - - St K β^ ·^10 - - − 4 - - ν 0 −V 0 στ 4 - -#### √ - - St S 0 β^ V^0 - - στ 4 - -#### √ - - St K β Θ φ · V 0 V 0 - -#### (√ - - St S 0 - -#### ( - - rf 2 +^ - - σ^2 8 - -#### ) - - β + rf φT γ - -#### ) - -#### V 0 - -#### ( - - rf 2 γ^ − - -#### √ - - St K - -#### ( - - rf 2 +^ - - σ^2 8 - -#### ) - - β - -#### ) - - ρ 0 −V 0 - -#### ( - - τ 2 - -#### √ - - St S 0 β^ +^ τ φT γ - -#### ) - - V 0 τ 2 - -#### ( - - β - -#### √ - - St K −^ γ - -#### ) - - -## References - -1. Angeris, Chitra, "Improved Price Oracles: Constant Function Market Makers", June 2020, arXiv:2003.10001 [q-fin.TR] - -2. Jensen, Pourpouneh, Nielsen, Ross, "THE HOMOGENEOUS PROPERTIES OF AUTOMATED MARKET MAKERS", arXiv:2105.02782 [q-fin.TR] - -3. Park, Andreas, "Conceptual Flaws of Decentralized Automated Market Making" (April 11, 2022). Available at SSRN: https://ssrn.com/abstract=3805750 or [http://dx.doi.org/10.2139/ssrn.3805750](http://dx.doi.org/10.2139/ssrn.3805750) - -4. Clark, Joseph, "The Replicating Portfolio of a Constant Product Market" (March 8, 2020). Available at SSRN: https://ssrn.com/abstract=3550601 or [http://dx.doi.org/10.2139/ssrn.3550601](http://dx.doi.org/10.2139/ssrn.3550601) - -5. Adams, Robinson, Zinsmeister, "Uniswap v2 Core", March 2020, available at: https://uniswap.org/whitepaper.pdf - -6. Adams, Keefer, Robinson, Salem, Zinsmeister, "Uniswap v3 Core", March 2021, available at: https://uniswap.org/whitepaper-v3.pdf - -7. Aigner, Dhaliwal, "UNISWAP: Impermanent Loss and Risk Profile of a Liquidity Provider", June 2021, arXiv:2106.14404 [q-fin.TR] - -8. Elsts, "LIQUIDITY MATH IN UNISWAP V3", 30 September 2021, available at: https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf - -9. Fukasawa, Masaaki and Maire, Basile and Wunsch, Marcus, Weighted variance swaps hedge against Impermanent Loss (April 27, 2022). Available at SSRN: https://ssrn.com/abstract=4095029 or [http://dx.doi.org/10.2139/ssrn.4095029](http://dx.doi.org/10.2139/ssrn.4095029) - -10. Black, Scholes, "The Pricing of Options and Corporate Liabilities", Journal of Political Economy Vol. 81, No. 3 (May - Jun, 1973), pp. 637-654, The University of Chicago Press - - diff --git a/test/samples/URL Sample.md b/test/samples/URL Sample.md deleted file mode 100644 index a952f62d..00000000 --- a/test/samples/URL Sample.md +++ /dev/null @@ -1,7 +0,0 @@ -# Sample PDF - -## This is a simple PDF file. Fun fun fun. - -Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus facilisis odio sed mi. Curabitur suscipit. Nullam vel nisi. Etiam semper ipsum ut lectus. Proin aliquam, erat eget pharetra commodo, eros mi condimentum quam, sed commodo justo quam ut velit. Integer a erat. Cras laoreet ligula cursus enim. Aenean scelerisque velit et tellus. Vestibulum dictum aliquet sem. Nulla facilisi. Vestibulum accumsan ante vitae elit. Nulla erat dolor, blandit in, rutrum quis, semper pulvinar, enim. Nullam varius congue risus. Vivamus sollicitudin, metus ut interdum eleifend, nisi tellus pellentesque elit, tristique accumsan eros quam et risus. Suspendisse libero odio, mattis sit amet, aliquet eget, hendrerit vel, nulla. Sed vitae augue. Aliquam erat volutpat. Aliquam feugiat vulputate nisl. Suspendisse quis nulla pretium ante pretium mollis. Proin velit ligula, sagittis at, egestas a, pulvinar quis, nisl. Pellentesque sit amet lectus. Praesent pulvinar, nunc quis iaculis sagittis, justo quam lobortis tortor, sed vestibulum dui metus venenatis est. Nunc cursus ligula. Nulla facilisi. Phasellus ullamcorper consectetuer ante. Duis tincidunt, urna id condimentum luctus, nibh ante vulputate sapien, id sagittis massa orci ut enim. Pellentesque vestibulum convallis sem. Nulla consequat quam ut nisl. Nullam est. Curabitur tincidunt dapibus lorem. Proin velit turpis, scelerisque sit amet, iaculis nec, rhoncus ac, ipsum. Phasellus lorem arcu, feugiat eu, gravida eu, consequat molestie, ipsum. Nullam vel est ut ipsum volutpat feugiat. Aenean pellentesque. In mauris. Pellentesque dui nisi, iaculis eu, rhoncus in, venenatis ac, ante. Ut odio justo, scelerisque vel, facilisis non, commodo a, pede. Cras nec massa sit amet tortor volutpat varius. Donec lacinia, neque a luctus aliquet, pede massa imperdiet ante, at varius lorem pede sed sapien. Fusce erat nibh, aliquet in, eleifend eget, commodo eget, erat. Fusce consectetuer. Cras risus tortor, porttitor nec, tristique sed, convallis semper, eros. Fusce vulputate ipsum a mauris. Phasellus mollis. Curabitur sed urna. Aliquam nec sapien non nibh pulvinar convallis. Vivamus facilisis augue quis quam. Proin cursus aliquet metus. Suspendisse lacinia. Nulla at tellus ac turpis eleifend scelerisque. Maecenas a pede vitae enim commodo interdum. Donec odio. Sed sollicitudin dui vitae justo. Morbi elit nunc, facilisis a, mollis a, molestie at, lectus. Suspendisse eget mauris eu tellus molestie cursus. Duis ut magna at justo dignissim condimentum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus varius. Ut sit amet diam suscipit mauris ornare aliquam. Sed varius. Duis arcu. Etiam tristique massa eget dui. Phasellus congue. Aenean est erat, tincidunt eget, venenatis quis, commodo at, quam. - -