openpondai/agents/polymarket-market-watcher
OpenTool app
1Branch0Tags
OP
OpenPond Syncsync: merge local template master into production
typescript
import { buildHyperliquidProfileAssets, normalizeHyperliquidBaseSymbol } from "opentool/adapters/hyperliquid";
import { z } from "zod";
const CONFIG_ENV = "OPENTOOL_PUBLIC_PM_MARKET_WATCHER_CONFIG";
const TEMPLATE_CONFIG_VERSION = 2;
const DEFAULT_ASSET = "BTC";
const DEFAULT_AMOUNT_USD = 200;
const DEFAULT_PERCENT_OF_EQUITY = 2;
const DEFAULT_MAX_PERCENT_OF_EQUITY = 10;
const DEFAULT_RESOLUTION = "60" as const;
const DEFAULT_COUNT_BACK = 240;
const DEFAULT_EXECUTION_ENV = "mainnet" as const;
const DEFAULT_EXECUTION_MODE = "long-only" as const;
const DEFAULT_SLIPPAGE_BPS = 50;
const scheduleSchema = z
.object({
cron: z.string().min(1),
enabled: z.boolean().optional(),
notifyEmail: z.boolean().optional(),
})
.strict();
const scheduleOverrideSchema = z
.object({
cron: z.string().min(1).optional(),
enabled: z.boolean().optional(),
notifyEmail: z.boolean().optional(),
})
.strict();
const executionSchema = z
.object({
enabled: z.boolean().optional(),
environment: z.enum(["testnet", "mainnet"]).optional(),
symbol: z.string().min(1).optional(),
mode: z.enum(["long-only", "long-short"]).optional(),
size: z.number().positive().optional(),
leverage: z.number().positive().optional(),
slippageBps: z.number().min(0).max(500).optional(),
})
.strict();
const baseConfigSchema = z
.object({
configVersion: z.number().int().optional(),
platform: z.literal("hyperliquid").optional(),
marketId: z.string().min(1).optional(),
marketSlug: z.string().min(1).optional(),
conditionId: z.string().min(1).optional(),
watchedOutcome: z.enum(["yes", "no"]).optional(),
minProbability: z.number().min(0).max(1).optional(),
minLiquidity: z.number().nonnegative().optional(),
action: z.enum(["buy", "sell"]).optional(),
onNoMatch: z.enum(["hold", "unknown"]).optional(),
asset: z.string().min(1).optional(),
marketSymbol: z.string().min(1).optional(),
allocationMode: z.enum(["fixed", "percent_equity"]).optional(),
amountUsd: z.number().positive().optional(),
percentOfEquity: z.number().positive().optional(),
maxPercentOfEquity: z.number().positive().optional(),
resolution: z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]).optional(),
countBack: z.number().int().min(50).max(5000).optional(),
historyFidelitySeconds: z.number().int().min(60).max(86400).optional(),
schedule: scheduleSchema.optional(),
execution: executionSchema.optional(),
})
.strict()
.refine((value) => Boolean(value.marketId || value.marketSlug || value.conditionId), {
message: "marketId, marketSlug, or conditionId is required",
path: ["marketSlug"],
});
export const requestOverrideSchema = z
.object({
marketId: z.string().min(1).optional(),
marketSlug: z.string().min(1).optional(),
conditionId: z.string().min(1).optional(),
watchedOutcome: z.enum(["yes", "no"]).optional(),
minProbability: z.number().min(0).max(1).optional(),
minLiquidity: z.number().nonnegative().optional(),
action: z.enum(["buy", "sell"]).optional(),
onNoMatch: z.enum(["hold", "unknown"]).optional(),
asset: z.string().min(1).optional(),
marketSymbol: z.string().min(1).optional(),
allocationMode: z.enum(["fixed", "percent_equity"]).optional(),
amountUsd: z.number().positive().optional(),
percentOfEquity: z.number().positive().optional(),
maxPercentOfEquity: z.number().positive().optional(),
resolution: z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]).optional(),
countBack: z.number().int().min(50).max(5000).optional(),
historyFidelitySeconds: z.number().int().min(60).max(86400).optional(),
schedule: scheduleOverrideSchema.optional(),
execution: executionSchema.optional(),
asOf: z.string().datetime().optional(),
})
.strict();
export type PolymarketMarketWatcherRequestOverrides = z.infer<typeof requestOverrideSchema>;
type PartialScheduleConfig = z.infer<typeof scheduleSchema>;
type PartialExecutionConfig = z.infer<typeof executionSchema>;
export type PolymarketWatcherSignal = "buy" | "sell" | "hold" | "unknown";
export type ScheduleConfig = {
cron: string;
enabled: boolean;
notifyEmail: boolean;
};
export type ExecutionConfig = {
enabled: boolean;
environment: "testnet" | "mainnet";
symbol?: string;
mode: "long-only" | "long-short";
size?: number;
leverage?: number;
slippageBps: number;
};
export type PolymarketMarketWatcherConfig = {
configVersion: number;
platform: "hyperliquid";
marketId?: string;
marketSlug?: string;
conditionId?: string;
watchedOutcome: "yes" | "no";
minProbability: number;
minLiquidity?: number;
action: "buy" | "sell";
onNoMatch: "hold" | "unknown";
asset: string;
marketSymbol?: string;
allocationMode: "fixed" | "percent_equity";
amountUsd?: number;
percentOfEquity: number;
maxPercentOfEquity: number;
resolution: "1" | "5" | "15" | "30" | "60" | "240" | "1D" | "1W";
countBack: number;
historyFidelitySeconds: number;
schedule: ScheduleConfig;
execution: ExecutionConfig;
};
const DEFAULT_SCHEDULE: ScheduleConfig = {
cron: "0 * * * *",
enabled: false,
notifyEmail: false,
};
const TEMPLATE_CONFIG_DEFAULTS: PolymarketMarketWatcherConfig = {
configVersion: TEMPLATE_CONFIG_VERSION,
platform: "hyperliquid",
marketSlug: "us-x-iran-ceasefire-by-march-31",
watchedOutcome: "no",
minProbability: 0.8,
minLiquidity: 100_000,
action: "buy",
onNoMatch: "hold",
asset: DEFAULT_ASSET,
allocationMode: "fixed",
amountUsd: DEFAULT_AMOUNT_USD,
percentOfEquity: DEFAULT_PERCENT_OF_EQUITY,
maxPercentOfEquity: DEFAULT_MAX_PERCENT_OF_EQUITY,
resolution: DEFAULT_RESOLUTION,
countBack: DEFAULT_COUNT_BACK,
historyFidelitySeconds: 3600,
schedule: DEFAULT_SCHEDULE,
execution: {
enabled: false,
environment: DEFAULT_EXECUTION_ENV,
mode: DEFAULT_EXECUTION_MODE,
slippageBps: DEFAULT_SLIPPAGE_BPS,
},
};
const TEMPLATE_CONFIG_SCHEMA = {
type: "object",
required: ["watchedOutcome", "minProbability", "asset"],
properties: {
configVersion: {
type: "number",
title: "Config version",
description: "Internal schema version for the market watcher defaults.",
readOnly: true,
"x-hidden": true,
},
platform: {
type: "string",
enum: ["hyperliquid"],
title: "Platform",
description: "Execution venue for this strategy.",
readOnly: true,
},
marketSlug: {
type: "string",
title: "Market slug",
description: "Exact Polymarket market slug to watch.",
},
marketId: {
type: "string",
title: "Market id",
description: "Exact Polymarket market id if you prefer id-based lookup.",
},
conditionId: {
type: "string",
title: "Condition id",
description: "Exact Polymarket condition id for the watched market.",
},
watchedOutcome: {
type: "string",
enum: ["yes", "no"],
title: "Watched outcome",
description: "Outcome probability to monitor on the chosen market.",
},
minProbability: {
type: "number",
title: "Minimum probability",
description: "Trigger once the watched outcome reaches at least this probability.",
minimum: 0,
maximum: 1,
},
minLiquidity: {
type: "number",
title: "Minimum liquidity",
description: "Optional liquidity floor for the watched market.",
minimum: 0,
},
action: {
type: "string",
enum: ["buy", "sell"],
title: "Trade action",
description: "Hyperliquid action to take when the threshold passes.",
},
onNoMatch: {
type: "string",
enum: ["hold", "unknown"],
title: "Fallback action",
description: "What to return when the threshold is not met.",
},
asset: {
type: "string",
title: "Asset",
description:
"Default Hyperliquid perp symbol. Use marketSymbol or execution.symbol for exact spot pairs (BASE-QUOTE or BASE/QUOTE) or HIP-3 symbols (dex:BASE).",
"x-format": "asset",
},
marketSymbol: {
type: "string",
title: "Exact market symbol",
description:
"Optional exact Hyperliquid symbol used for replay and execution. Use BASE-QUOTE or BASE/QUOTE for spot, bare BASE for perps, or dex:BASE for HIP-3.",
},
allocationMode: {
type: "string",
enum: ["fixed", "percent_equity"],
title: "Allocation mode",
description: "Fixed USD or percent-of-equity sizing.",
},
amountUsd: {
type: "number",
title: "Amount USD",
description: "Fixed notional size when allocation mode is fixed.",
minimum: 1,
},
percentOfEquity: {
type: "number",
title: "Percent of equity",
description: "Sizing target when allocation mode is percent-of-equity.",
minimum: 0.1,
maximum: 100,
},
maxPercentOfEquity: {
type: "number",
title: "Max percent of equity",
description: "Absolute sizing cap for percent-of-equity mode.",
minimum: 0.1,
maximum: 100,
},
resolution: {
type: "string",
enum: ["1", "5", "15", "30", "60", "240", "1D", "1W"],
title: "Bar resolution",
description: "Resolution used for Hyperliquid price alignment in backtests and live reads.",
},
historyFidelitySeconds: {
type: "number",
title: "Polymarket history fidelity",
description: "Sampling interval in seconds for Polymarket price-history replay.",
minimum: 60,
maximum: 86400,
},
schedule: {
type: "object",
title: "Schedule",
properties: {
cron: { type: "string", title: "Cron expression" },
enabled: { type: "boolean", title: "Enabled" },
notifyEmail: { type: "boolean", title: "Notify email" },
},
},
execution: {
type: "object",
title: "Execution",
properties: {
enabled: { type: "boolean", title: "Enable live execution" },
environment: {
type: "string",
enum: ["testnet", "mainnet"],
title: "Environment",
},
symbol: {
type: "string",
title: "Execution symbol",
description:
"Optional exact Hyperliquid symbol override. Use BASE-QUOTE or BASE/QUOTE for spot, bare BASE for perps, or dex:BASE for HIP-3.",
},
mode: {
type: "string",
enum: ["long-only", "long-short"],
title: "Execution mode",
},
size: {
type: "number",
title: "Override size",
minimum: 0.00000001,
},
leverage: {
type: "number",
title: "Leverage",
minimum: 1,
maximum: 50,
},
slippageBps: {
type: "number",
title: "Slippage bps",
minimum: 0,
maximum: 500,
},
},
},
},
};
export const POLYMARKET_MARKET_WATCHER_TEMPLATE_CONFIG = {
version: TEMPLATE_CONFIG_VERSION,
schema: TEMPLATE_CONFIG_SCHEMA,
defaults: TEMPLATE_CONFIG_DEFAULTS,
envVar: CONFIG_ENV,
};
function parseJson(raw: string | null): unknown {
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function mergeSchedule(config?: PartialScheduleConfig): ScheduleConfig {
return {
cron: config?.cron ?? DEFAULT_SCHEDULE.cron,
enabled: config?.enabled ?? DEFAULT_SCHEDULE.enabled,
notifyEmail: config?.notifyEmail ?? DEFAULT_SCHEDULE.notifyEmail,
};
}
function mergeExecution(config?: PartialExecutionConfig): ExecutionConfig {
return {
enabled: config?.enabled ?? TEMPLATE_CONFIG_DEFAULTS.execution.enabled,
environment: config?.environment ?? TEMPLATE_CONFIG_DEFAULTS.execution.environment,
...(config?.symbol?.trim() ? { symbol: config.symbol.trim() } : {}),
mode: config?.mode ?? TEMPLATE_CONFIG_DEFAULTS.execution.mode,
...(typeof config?.size === "number" ? { size: config.size } : {}),
...(typeof config?.leverage === "number" ? { leverage: config.leverage } : {}),
slippageBps: config?.slippageBps ?? TEMPLATE_CONFIG_DEFAULTS.execution.slippageBps,
};
}
function normalizeBaseSymbol(value: string): string {
const normalized = normalizeHyperliquidBaseSymbol(value);
if (normalized) return normalized;
return value.trim().toUpperCase();
}
function normalizeConfig(
raw: z.infer<typeof baseConfigSchema>,
): PolymarketMarketWatcherConfig {
return {
configVersion: TEMPLATE_CONFIG_VERSION,
platform: "hyperliquid",
...(raw.marketId?.trim() ? { marketId: raw.marketId.trim() } : {}),
...(raw.marketSlug?.trim() ? { marketSlug: raw.marketSlug.trim() } : {}),
...(raw.conditionId?.trim() ? { conditionId: raw.conditionId.trim() } : {}),
watchedOutcome: raw.watchedOutcome ?? TEMPLATE_CONFIG_DEFAULTS.watchedOutcome,
minProbability: raw.minProbability ?? TEMPLATE_CONFIG_DEFAULTS.minProbability,
...(typeof raw.minLiquidity === "number" ? { minLiquidity: raw.minLiquidity } : {}),
action: raw.action ?? TEMPLATE_CONFIG_DEFAULTS.action,
onNoMatch: raw.onNoMatch ?? TEMPLATE_CONFIG_DEFAULTS.onNoMatch,
asset: normalizeBaseSymbol(raw.asset ?? TEMPLATE_CONFIG_DEFAULTS.asset),
...(raw.marketSymbol?.trim() ? { marketSymbol: raw.marketSymbol.trim() } : {}),
allocationMode: raw.allocationMode ?? TEMPLATE_CONFIG_DEFAULTS.allocationMode,
amountUsd: raw.amountUsd ?? TEMPLATE_CONFIG_DEFAULTS.amountUsd,
percentOfEquity: raw.percentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.percentOfEquity,
maxPercentOfEquity:
raw.maxPercentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.maxPercentOfEquity,
resolution: raw.resolution ?? TEMPLATE_CONFIG_DEFAULTS.resolution,
countBack: raw.countBack ?? TEMPLATE_CONFIG_DEFAULTS.countBack,
historyFidelitySeconds:
raw.historyFidelitySeconds ?? TEMPLATE_CONFIG_DEFAULTS.historyFidelitySeconds,
schedule: mergeSchedule(raw.schedule),
execution: mergeExecution(raw.execution),
};
}
export function resolveRuntimeConfig(
overrides?: PolymarketMarketWatcherRequestOverrides,
): PolymarketMarketWatcherConfig {
const baseRaw = parseJson(process.env[CONFIG_ENV] ?? null);
const parsed = baseConfigSchema.safeParse(baseRaw ?? TEMPLATE_CONFIG_DEFAULTS);
const base = parsed.success ? parsed.data : TEMPLATE_CONFIG_DEFAULTS;
const merged = {
...base,
...overrides,
schedule: {
cron: overrides?.schedule?.cron ?? base.schedule?.cron ?? DEFAULT_SCHEDULE.cron,
enabled: overrides?.schedule?.enabled ?? base.schedule?.enabled,
notifyEmail: overrides?.schedule?.notifyEmail ?? base.schedule?.notifyEmail,
},
execution: {
...base.execution,
...overrides?.execution,
},
};
return normalizeConfig(merged);
}
export function resolveScheduleConfig(config: PolymarketMarketWatcherConfig) {
if (!config.schedule.cron) return undefined;
return {
cron: config.schedule.cron,
enabled: config.schedule.enabled,
notifyEmail: config.schedule.notifyEmail,
};
}
export function resolveProfileAssets(config: PolymarketMarketWatcherConfig) {
return buildHyperliquidProfileAssets({
environment: config.execution.environment,
assets: [{ assetSymbols: [config.asset] }],
});
}