Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

unusualActivity.ts7.18 kB
/** * Unusual options activity detection * * Identifies anomalous trading patterns that may indicate informed trading. */ import type { OptionsChain, OptionContract, UnusualActivity, UnusualActivityReport } from "./types"; export interface DetectUnusualActivityParams { /** Options chain data */ chain: OptionsChain; /** Minimum volume to consider (default: 100) */ minVolume?: number; /** Volume/OI ratio threshold for spike detection (default: 0.5) */ volumeOiThreshold?: number; /** Percentile threshold for "unusual" (default: 90) */ percentileThreshold?: number; /** Maximum number of results to return (default: 20) */ maxResults?: number; } /** * Detect unusual options activity in an options chain */ export function detectUnusualActivity( params: DetectUnusualActivityParams ): UnusualActivityReport { const { chain, minVolume = 100, volumeOiThreshold = 0.5, percentileThreshold = 90, maxResults = 20, } = params; const activities: UnusualActivity[] = []; const spot = chain.underlyingPrice; // Calculate volume statistics for percentile-based detection const volumes = chain.contracts .filter((c) => c.volume > 0) .map((c) => c.volume); if (volumes.length === 0) { return createEmptyReport(chain); } const volumeThreshold = percentileValue(volumes, percentileThreshold); // Analyze each contract for (const contract of chain.contracts) { if (contract.volume < minVolume) continue; const volumeToOiRatio = contract.openInterest > 0 ? contract.volume / contract.openInterest : contract.volume; const isWing = isWingStrike(contract, spot); const moneyness = contract.strike / spot; // Check for various unusual activity patterns const detectedActivities: Omit<UnusualActivity, "contract">[] = []; // 1. Volume spike (high absolute volume) if (contract.volume >= volumeThreshold) { detectedActivities.push({ type: "volume_spike", score: Math.min(100, Math.round((contract.volume / volumeThreshold) * 50)), description: `Volume ${contract.volume.toLocaleString()} exceeds ${percentileThreshold}th percentile`, volumeToOiRatio, sentiment: inferSentiment(contract, spot), }); } // 2. High volume to OI ratio (potential opening positions) if (volumeToOiRatio >= volumeOiThreshold && contract.volume >= minVolume) { detectedActivities.push({ type: "oi_change", score: Math.min(100, Math.round(volumeToOiRatio * 40)), description: `Vol/OI ratio ${volumeToOiRatio.toFixed(2)} suggests new position opening`, volumeToOiRatio, sentiment: inferSentiment(contract, spot), }); } // 3. Wing activity (OTM options with high volume) if (isWing && contract.volume >= volumeThreshold * 0.5) { const wingDesc = moneyness < 0.9 ? "deep OTM put" : moneyness > 1.1 ? "deep OTM call" : "wing"; detectedActivities.push({ type: "wing_activity", score: Math.min(100, Math.round((contract.volume / volumeThreshold) * 60)), description: `Unusual ${wingDesc} activity at ${(moneyness * 100).toFixed(0)}% moneyness`, volumeToOiRatio, sentiment: inferSentiment(contract, spot), }); } // 4. Large single trades (volume >> average) if (contract.volume >= volumeThreshold * 2) { detectedActivities.push({ type: "large_trade", score: Math.min(100, Math.round((contract.volume / volumeThreshold) * 30)), description: `Large trade: ${contract.volume.toLocaleString()} contracts`, volumeToOiRatio, sentiment: inferSentiment(contract, spot), }); } // Add the highest-scoring activity for this contract if (detectedActivities.length > 0) { const best = detectedActivities.sort((a, b) => b.score - a.score)[0]; activities.push({ contract, ...best, }); } } // Sort by score and limit results activities.sort((a, b) => b.score - a.score); const topActivities = activities.slice(0, maxResults); // Calculate summary stats const callActivities = topActivities.filter((a) => a.contract.right === "call"); const putActivities = topActivities.filter((a) => a.contract.right === "put"); const bullishCount = topActivities.filter((a) => a.sentiment === "bullish").length; const bearishCount = topActivities.filter((a) => a.sentiment === "bearish").length; let dominantSentiment: "bullish" | "bearish" | "mixed" | "neutral" = "neutral"; if (bullishCount > bearishCount * 1.5) { dominantSentiment = "bullish"; } else if (bearishCount > bullishCount * 1.5) { dominantSentiment = "bearish"; } else if (bullishCount > 0 || bearishCount > 0) { dominantSentiment = "mixed"; } // Get top strikes by activity const strikeCounts = new Map<number, number>(); for (const a of topActivities) { const count = strikeCounts.get(a.contract.strike) || 0; strikeCounts.set(a.contract.strike, count + 1); } const topStrikes = [...strikeCounts.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([strike]) => strike); return { symbol: chain.symbol, asOf: chain.asOf, spot, activities: topActivities, summary: { totalUnusualContracts: topActivities.length, callVsPutRatio: putActivities.length > 0 ? callActivities.length / putActivities.length : callActivities.length > 0 ? Infinity : 0, dominantSentiment, topStrikes, }, }; } /** * Check if a strike is a "wing" (far OTM) */ function isWingStrike(contract: OptionContract, spot: number): boolean { const moneyness = contract.strike / spot; if (contract.right === "call") { return moneyness > 1.1; // 10%+ OTM call } else { return moneyness < 0.9; // 10%+ OTM put } } /** * Infer sentiment from option activity */ function inferSentiment( contract: OptionContract, spot: number ): "bullish" | "bearish" | "neutral" { const moneyness = contract.strike / spot; if (contract.right === "call") { // Call buying is generally bullish // But deep ITM calls might be hedging (neutral) if (moneyness < 0.95) return "neutral"; // Deep ITM return "bullish"; } else { // Put buying is generally bearish // But deep ITM puts might be hedging (neutral) if (moneyness > 1.05) return "neutral"; // Deep ITM return "bearish"; } } /** * Calculate percentile value from array */ function percentileValue(arr: number[], p: number): number { const sorted = [...arr].sort((a, b) => a - b); const idx = Math.ceil((p / 100) * sorted.length) - 1; return sorted[Math.max(0, idx)]; } /** * Create empty report when no data available */ function createEmptyReport(chain: OptionsChain): UnusualActivityReport { return { symbol: chain.symbol, asOf: chain.asOf, spot: chain.underlyingPrice, activities: [], summary: { totalUnusualContracts: 0, callVsPutRatio: 0, dominantSentiment: "neutral", topStrikes: [], }, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ademscodeisnotsobad/Quant-Companion-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server