/**
* MCP Tool: detect_unusual_activity
*
* Identify unusual options activity that may indicate informed trading.
*/
import { z } from "zod";
import { detectUnusualActivity, type QuantError } from "@quant-companion/core";
import { getDefaultProvider } from "../marketData";
import { createError } from "../errors";
export const detectUnusualActivitySchema = z.object({
symbol: z.string().describe("Stock/ETF ticker symbol (e.g., AAPL, NVDA, SPY)"),
minVolume: z
.number()
.min(1)
.default(100)
.describe("Minimum volume to consider (default: 100)"),
volumeOiThreshold: z
.number()
.min(0)
.default(0.5)
.describe("Volume/OI ratio threshold for detection (default: 0.5)"),
maxResults: z
.number()
.min(1)
.max(50)
.default(20)
.describe("Maximum results to return (default: 20)"),
});
export type DetectUnusualActivityInput = z.infer<typeof detectUnusualActivitySchema>;
export interface DetectUnusualActivityOutput {
symbol: string;
spot: number;
asOf: string;
summary: {
totalUnusualContracts: number;
callVsPutRatio: number;
dominantSentiment: "bullish" | "bearish" | "mixed" | "neutral";
topStrikes: number[];
};
activities: Array<{
optionSymbol: string;
right: "call" | "put";
strike: number;
expiration: string;
type: string;
score: number;
description: string;
volume: number;
openInterest: number;
volumeToOiRatio: number;
sentiment: "bullish" | "bearish" | "neutral";
}>;
interpretation: {
headline: string;
details: string[];
};
}
export const detectUnusualActivityDefinition = {
name: "detect_unusual_activity",
description: `Detect unusual options activity for a symbol.
Identifies:
- Volume spikes (abnormally high trading)
- High Vol/OI ratios (new position opening)
- Wing activity (far OTM trades)
- Large trades
Returns:
- Summary with dominant sentiment
- Top unusual contracts with scores
- Interpretation and headline
Use this for:
- Flow analysis (like Unusual Whales)
- Detecting potential informed trading
- Sentiment analysis from options markets`,
inputSchema: {
type: "object" as const,
properties: {
symbol: { type: "string", description: "Stock/ETF ticker symbol" },
minVolume: {
type: "number",
description: "Minimum volume filter (default: 100)",
default: 100,
},
volumeOiThreshold: {
type: "number",
description: "Vol/OI threshold (default: 0.5)",
default: 0.5,
},
maxResults: {
type: "number",
description: "Max results (default: 20)",
default: 20,
},
},
required: ["symbol"],
},
};
function generateInterpretation(
symbol: string,
output: DetectUnusualActivityOutput
): DetectUnusualActivityOutput["interpretation"] {
const { summary, activities } = output;
const details: string[] = [];
// Generate headline
let headline: string;
if (summary.totalUnusualContracts === 0) {
headline = `No unusual options activity detected for ${symbol}`;
} else {
const sentimentWord =
summary.dominantSentiment === "bullish"
? "bullish"
: summary.dominantSentiment === "bearish"
? "bearish"
: "mixed";
headline = `${summary.totalUnusualContracts} unusual contracts detected with ${sentimentWord} sentiment`;
}
// Add details
if (summary.callVsPutRatio > 2) {
details.push(`Heavy call activity (${summary.callVsPutRatio.toFixed(1)}x calls vs puts)`);
} else if (summary.callVsPutRatio < 0.5) {
details.push(`Heavy put activity (${(1 / summary.callVsPutRatio).toFixed(1)}x puts vs calls)`);
}
if (summary.topStrikes.length > 0) {
details.push(`Key strikes: ${summary.topStrikes.slice(0, 3).join(", ")}`);
}
// Highlight top activities
const topByScore = activities.slice(0, 3);
for (const a of topByScore) {
details.push(`${a.right.toUpperCase()} $${a.strike} (${a.expiration}): ${a.description}`);
}
// Check for wing trades
const wingTrades = activities.filter((a) => a.type === "wing_activity");
if (wingTrades.length > 0) {
details.push(`${wingTrades.length} wing trades detected (potential tail hedging or speculation)`);
}
return { headline, details };
}
export async function detectUnusualActivityTool(
input: DetectUnusualActivityInput
): Promise<DetectUnusualActivityOutput | { error: QuantError }> {
const provider = getDefaultProvider();
try {
const chain = await provider.getOptionsChain({
symbol: input.symbol.toUpperCase(),
});
const report = detectUnusualActivity({
chain,
minVolume: input.minVolume,
volumeOiThreshold: input.volumeOiThreshold,
maxResults: input.maxResults,
});
const activities = report.activities.map((a) => ({
optionSymbol: a.contract.optionSymbol,
right: a.contract.right,
strike: a.contract.strike,
expiration: a.contract.expiration,
type: a.type,
score: a.score,
description: a.description,
volume: a.contract.volume,
openInterest: a.contract.openInterest,
volumeToOiRatio: a.volumeToOiRatio,
sentiment: a.sentiment,
}));
const output: DetectUnusualActivityOutput = {
symbol: report.symbol,
spot: report.spot,
asOf: report.asOf,
summary: report.summary,
activities,
interpretation: { headline: "", details: [] },
};
output.interpretation = generateInterpretation(input.symbol.toUpperCase(), output);
return output;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
error: createError("DATA_NOT_FOUND", `Failed to detect unusual activity: ${message}`),
};
}
}