import { z } from "zod";
import { api } from "../services/api.js";
import { getPrice, getSpread, getBook } from "../services/clob-public.js";
const schema = z.object({
slug: z.string().describe("Market slug to analyze"),
});
export const analyzeMarketOpportunityTool = {
name: "analyze_market_opportunity",
description:
"Heuristic market analysis using volume, liquidity, spread, and time-to-close. Uses first clobTokenIds entry when available. Example: slug=will-biden-win-2024.",
parameters: schema,
execute: async (args: z.infer<typeof schema>) => {
try {
const market: any = await api.getMarketBySlug(args.slug);
// Parse token IDs
let tokenIds: string[] = [];
try {
tokenIds =
typeof market.clobTokenIds === "string"
? JSON.parse(market.clobTokenIds)
: market.clobTokenIds ?? [];
} catch {
// ignore
}
// Gather metrics for first token
let spreadData = { spread: 0, bid: 0, ask: 0 };
let bookDepth = { bidLevels: 0, askLevels: 0, totalLiquidity: 0 };
if (tokenIds.length > 0) {
try {
spreadData = await getSpread(tokenIds[0]);
const book = await getBook(tokenIds[0]);
const liq = [...book.bids, ...book.asks].reduce(
(s, l) => s + Number.parseFloat(l.price) * Number.parseFloat(l.size),
0,
);
bookDepth = {
bidLevels: book.bids.length,
askLevels: book.asks.length,
totalLiquidity: liq,
};
} catch {
// continue with defaults
}
}
// Heuristic scoring
const volume24hr = Number.parseFloat(market.volume24hr ?? "0");
const liquidity = Number.parseFloat(market.liquidity ?? "0");
const endDate = market.endDate ? new Date(market.endDate) : null;
const hoursToClose = endDate
? (endDate.getTime() - Date.now()) / (1000 * 60 * 60)
: null;
let score = 50; // base
// Volume score (0-20)
if (volume24hr > 100000) score += 20;
else if (volume24hr > 10000) score += 15;
else if (volume24hr > 1000) score += 10;
else if (volume24hr > 100) score += 5;
// Liquidity score (0-15)
if (liquidity > 50000) score += 15;
else if (liquidity > 10000) score += 10;
else if (liquidity > 1000) score += 5;
// Spread score (0-15, tighter is better)
if (spreadData.spread > 0 && spreadData.spread < 0.02) score += 15;
else if (spreadData.spread < 0.05) score += 10;
else if (spreadData.spread < 0.1) score += 5;
// Time penalty for very short or very long
if (hoursToClose !== null) {
if (hoursToClose < 1) score -= 10;
else if (hoursToClose < 24) score += 5;
}
const riskLevel =
score >= 80 ? "LOW" : score >= 60 ? "MEDIUM" : score >= 40 ? "HIGH" : "VERY_HIGH";
return JSON.stringify(
{
market: {
slug: market.slug,
question: market.question,
volume24hr,
liquidity,
endDate: market.endDate,
hoursToClose: hoursToClose?.toFixed(1),
},
analysis: {
score,
riskLevel,
spread: spreadData,
bookDepth,
recommendation:
score >= 70
? "Market has good liquidity and tight spreads — suitable for analysis."
: score >= 50
? "Market has moderate activity — check spread and depth before acting."
: "Market has low activity or wide spreads — exercise caution.",
},
},
null,
2,
);
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : String(error) });
}
},
};