openpondai/agents/polymarket-market-watcher

OpenTool app

1Branch0Tags
OP
OpenPond Syncsync: merge local template master into production
typescript
import { fetchHyperliquidBars, normalizeHyperliquidIndicatorBars, resolveHyperliquidTargetSize, } from "opentool/adapters/hyperliquid"; import { fetchPolymarketMarket, fetchPolymarketMidpoint, fetchPolymarketPriceHistory, type PolymarketMarket, } from "opentool/adapters/polymarket"; import { buildBacktestDecisionSeriesInput, resolveBacktestAccountValueUsd, resolveBacktestWindow, type BacktestDecisionRequest, type BacktestResolution, } from "opentool/backtest"; import type { PolymarketMarketWatcherConfig, PolymarketWatcherSignal } from "./config"; import { executePolymarketWatcherTrade } from "./execution"; import { resolveExecutableMarketSymbol } from "./market-symbol"; export type ResolvedPolymarketWatcherMarket = { title: string; marketId: string; slug: string | null; conditionId: string | null; tokenId: string; watchedOutcome: "yes" | "no"; probability: number; probabilitySource: "outcomePrices" | "midpoint"; liquidity: number | null; volume: number | null; }; export type PolymarketWatcherDecision = { signal: PolymarketWatcherSignal; reason: string; probability: number | null; liquidity: number | null; volume: number | null; }; export type PolymarketWatcherDecisionPoint = { ts: string; price: number; signal: PolymarketWatcherSignal; targetSize: number; budgetUsd: number; indicator: "polymarket"; indicators: Record<string, unknown>; }; export type PolymarketWatcherBacktestDecisionSeriesResult = { symbol: string; timeframeStart: string; timeframeEnd: string; barsEvaluated: number; resolution: BacktestResolution; indicator: "polymarket"; decisions: PolymarketWatcherDecisionPoint[]; }; export type PolymarketWatcherRunResult = { ok: true; fetchedAt: string; asOf: string; price: number; market: ResolvedPolymarketWatcherMarket; decision: PolymarketWatcherDecision; execution: Record<string, unknown>; allocation: { mode: PolymarketMarketWatcherConfig["allocationMode"]; percentOfEquity: number | undefined; maxPercentOfEquity: number | undefined; amountUsd: number | undefined; }; }; function resolutionToSeconds(resolution: BacktestResolution): number { if (resolution === "1") return 60; if (resolution === "5") return 300; if (resolution === "15") return 900; if (resolution === "30") return 1_800; if (resolution === "60") return 3_600; if (resolution === "240") return 14_400; if (resolution === "1D") return 86_400; return 604_800; } function normalizeOutcome(value: string | null | undefined): string { return (value ?? "").trim().toLowerCase(); } function parseOptionalNumeric(value: string | null | undefined): number | null { if (!value) return null; const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function resolveOutcomeIndex( market: PolymarketMarket, watchedOutcome: "yes" | "no", ): number { const normalizedOutcomes = (market.outcomes ?? []).map((outcome) => normalizeOutcome(outcome)); const exactIndex = normalizedOutcomes.findIndex((outcome) => outcome === watchedOutcome); if (exactIndex >= 0) return exactIndex; if (normalizedOutcomes.length === 2) { return watchedOutcome === "yes" ? 0 : 1; } throw new Error(`Unable to find watched outcome '${watchedOutcome}' on Polymarket market.`); } async function resolveWatchedMarket( config: PolymarketMarketWatcherConfig, ): Promise<ResolvedPolymarketWatcherMarket> { const market = await fetchPolymarketMarket({ ...(config.marketId ? { id: config.marketId } : {}), ...(config.marketSlug ? { slug: config.marketSlug } : {}), ...(config.conditionId ? { conditionId: config.conditionId } : {}), }); if (!market) { throw new Error("Polymarket market not found."); } const outcomeIndex = resolveOutcomeIndex(market, config.watchedOutcome); const tokenId = market.clobTokenIds?.[outcomeIndex]; if (!tokenId) { throw new Error("Polymarket market is missing a token id for the watched outcome."); } const outcomeProbability = market.outcomePrices?.[outcomeIndex] ?? null; const midpoint = outcomeProbability == null ? await fetchPolymarketMidpoint({ tokenId }) : null; const probability = outcomeProbability ?? midpoint; if (probability == null) { throw new Error("Unable to resolve watched Polymarket probability."); } return { title: market.question ?? market.slug ?? market.id, marketId: market.id, slug: market.slug ?? null, conditionId: market.conditionId ?? null, tokenId, watchedOutcome: config.watchedOutcome, probability, probabilitySource: outcomeProbability != null ? "outcomePrices" : "midpoint", liquidity: parseOptionalNumeric(market.liquidity), volume: parseOptionalNumeric(market.volume), }; } export function resolvePolymarketWatcherDecision(params: { config: PolymarketMarketWatcherConfig; market: Pick<ResolvedPolymarketWatcherMarket, "title" | "watchedOutcome" | "probability" | "liquidity" | "volume">; }): PolymarketWatcherDecision { const { config, market } = params; if ( typeof config.minLiquidity === "number" && market.liquidity != null && market.liquidity < config.minLiquidity ) { return { signal: config.onNoMatch, reason: `market liquidity ${market.liquidity.toFixed(2)} below ${config.minLiquidity.toFixed(2)}`, probability: market.probability, liquidity: market.liquidity, volume: market.volume, }; } if (market.probability < config.minProbability) { return { signal: config.onNoMatch, reason: `${market.watchedOutcome} probability ${market.probability.toFixed(4)} below ${config.minProbability.toFixed(4)}`, probability: market.probability, liquidity: market.liquidity, volume: market.volume, }; } return { signal: config.action, reason: `${market.watchedOutcome} probability ${market.probability.toFixed(4)} on ${market.title}`, probability: market.probability, liquidity: market.liquidity, volume: market.volume, }; } function findAlignedBar( bars: ReturnType<typeof normalizeHyperliquidIndicatorBars>, targetSeconds: number, startIndex: number, ): { bar: ReturnType<typeof normalizeHyperliquidIndicatorBars>[number]; nextIndex: number } { let index = startIndex; const targetMs = targetSeconds * 1000; while (index + 1 < bars.length && bars[index + 1]!.time <= targetMs) { index += 1; } const bar = bars[index] ?? bars[0]; if (!bar) { throw new Error("No Hyperliquid bars returned."); } return { bar, nextIndex: index }; } async function fetchCurrentPrice(config: PolymarketMarketWatcherConfig): Promise<{ asOf: string; price: number }> { const symbol = await resolveExecutableMarketSymbol(config); const bars = await fetchHyperliquidBars({ symbol, resolution: config.resolution, countBack: Math.max(2, Math.min(config.countBack, 10)), }); const bar = bars[bars.length - 1]; if (!bar) { throw new Error("No Hyperliquid price data returned."); } return { asOf: new Date(bar.time).toISOString(), price: bar.close, }; } async function fetchPriceHistory(params: { tokenId: string; resolution: BacktestResolution; startTs?: number; endTs?: number; fidelitySeconds: number; }) { return fetchPolymarketPriceHistory({ tokenId: params.tokenId, ...(params.startTs ? { startTs: params.startTs } : {}), ...(params.endTs ? { endTs: params.endTs } : {}), fidelity: Math.max(params.fidelitySeconds, resolutionToSeconds(params.resolution)), }); } async function fetchHyperliquidBarsForWindow(params: { symbol: string; resolution: BacktestResolution; countBack: number; fromSeconds?: number; toSeconds?: number; }) { return normalizeHyperliquidIndicatorBars( await fetchHyperliquidBars({ symbol: params.symbol, resolution: params.resolution, countBack: params.countBack, ...(params.fromSeconds != null ? { fromSeconds: params.fromSeconds } : {}), ...(params.toSeconds != null ? { toSeconds: params.toSeconds } : {}), }), ); } export async function buildBacktestDecisionSeries(params: { config: PolymarketMarketWatcherConfig; request: Partial<BacktestDecisionRequest>; }): Promise<PolymarketWatcherBacktestDecisionSeriesResult> { const input = buildBacktestDecisionSeriesInput(params.request); const window = resolveBacktestWindow({ fallbackCountBack: params.config.countBack, lookbackDays: input.lookbackDays, resolution: params.config.resolution, from: input.from, to: input.to, timeframeStart: input.timeframeStart, timeframeEnd: input.timeframeEnd, }); const config = input.symbol?.trim() ? { ...params.config, asset: input.symbol.trim(), marketSymbol: input.symbol.trim(), } : params.config; const nowSeconds = Math.floor(Date.now() / 1000); const lookbackWindowSeconds = typeof input.lookbackDays === "number" && Number.isFinite(input.lookbackDays) ? Math.max(1, Math.ceil(input.lookbackDays * 86400)) : Math.max( resolutionToSeconds(config.resolution) * Math.max(window.countBack - 5, 1), 86400, ); const historyStartSeconds = window.fromSeconds ?? Math.max(0, nowSeconds - lookbackWindowSeconds); const historyEndSeconds = window.toSeconds ?? nowSeconds; const market = await resolveWatchedMarket(config); const symbol = await resolveExecutableMarketSymbol(config); const bars = await fetchHyperliquidBarsForWindow({ symbol, resolution: config.resolution, countBack: window.countBack, fromSeconds: historyStartSeconds, toSeconds: historyEndSeconds, }); if (bars.length === 0) { throw new Error("No Hyperliquid bars returned."); } const history = await fetchPriceHistory({ tokenId: market.tokenId, resolution: config.resolution, startTs: historyStartSeconds, endTs: historyEndSeconds, fidelitySeconds: config.historyFidelitySeconds, }); if (history.length === 0) { throw new Error("No Polymarket price history returned."); } const resolvedAccountValue = resolveBacktestAccountValueUsd(input.accountValueUsd) ?? (config.allocationMode === "percent_equity" ? 10_000 : null); const decisions: PolymarketWatcherDecisionPoint[] = []; let barIndex = 0; for (const point of history) { const decision = resolvePolymarketWatcherDecision({ config, market: { title: market.title, watchedOutcome: market.watchedOutcome, probability: point.p, liquidity: market.liquidity, volume: market.volume, }, }); const { bar, nextIndex } = findAlignedBar(bars, point.t, barIndex); barIndex = nextIndex; const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({ config, execution: config.execution, accountValue: resolvedAccountValue, currentPrice: bar.close, }); decisions.push({ ts: new Date(point.t * 1000).toISOString(), price: bar.close, signal: decision.signal, targetSize, budgetUsd, indicator: "polymarket", indicators: { marketTitle: market.title, marketSlug: market.slug, conditionId: market.conditionId, tokenId: market.tokenId, watchedOutcome: market.watchedOutcome, probability: point.p, minProbability: config.minProbability, liquidity: market.liquidity, volume: market.volume, reason: decision.reason, }, }); } return { symbol, timeframeStart: decisions[0]?.ts ?? new Date().toISOString(), timeframeEnd: decisions[decisions.length - 1]?.ts ?? new Date().toISOString(), barsEvaluated: decisions.length, resolution: config.resolution, indicator: "polymarket", decisions, }; } export async function runSignalBot( config: PolymarketMarketWatcherConfig, ): Promise<PolymarketWatcherRunResult> { const [market, { asOf, price }] = await Promise.all([ resolveWatchedMarket(config), fetchCurrentPrice(config), ]); const decision = resolvePolymarketWatcherDecision({ config, market }); const execution = await executePolymarketWatcherTrade({ config, tradeSignal: decision.signal, currentPrice: price, }); if (!execution.ok) { throw new Error(execution.error); } return { ok: true, fetchedAt: new Date().toISOString(), asOf, price, market, decision, execution: execution.execution, allocation: { mode: config.allocationMode, percentOfEquity: config.percentOfEquity, maxPercentOfEquity: config.maxPercentOfEquity, amountUsd: config.amountUsd, }, }; }