openpondai/agents/polymarket-market-watcher
OpenTool app
1Branch0Tags
OP
OpenPond Syncsync: merge local template master into production
typescript
import {
buildHyperliquidMarketIdentity,
fetchHyperliquidClearinghouseState,
fetchHyperliquidSizeDecimals,
fetchHyperliquidSpotAccountValue,
fetchHyperliquidSpotClearinghouseState,
formatHyperliquidMarketablePrice,
formatHyperliquidOrderSize,
isHyperliquidSpotSymbol,
normalizeHyperliquidBaseSymbol,
placeHyperliquidOrder,
planHyperliquidTrade,
readHyperliquidAccountValue,
readHyperliquidPerpPositionSize,
readHyperliquidSpotBalanceSize,
resolveHyperliquidChainConfig,
resolveHyperliquidErrorDetail,
resolveHyperliquidLeverageMode,
resolveHyperliquidPair,
resolveHyperliquidSymbol,
resolveHyperliquidTargetSize,
updateHyperliquidLeverage,
type HyperliquidEnvironment,
type HyperliquidOrderResponse,
} from "opentool/adapters/hyperliquid";
import { store } from "opentool/store";
import { wallet } from "opentool/wallet";
import type { WalletFullContext } from "opentool/wallet";
import type { PolymarketMarketWatcherConfig, PolymarketWatcherSignal } from "./config";
import { resolveExecutableMarketSymbol } from "./market-symbol";
type LiveExecutionResult =
| {
ok: true;
execution: Record<string, unknown>;
}
| {
ok: false;
error: string;
execution: Record<string, unknown>;
};
export async function executePolymarketWatcherTrade(params: {
config: PolymarketMarketWatcherConfig;
tradeSignal: PolymarketWatcherSignal;
currentPrice: number;
}): Promise<LiveExecutionResult> {
const { config, tradeSignal, currentPrice } = params;
const executionConfig = config.execution;
if (!executionConfig.enabled) {
return {
ok: true,
execution: {
enabled: false,
action: "disabled",
signal: tradeSignal,
},
};
}
const environment = executionConfig.environment as HyperliquidEnvironment;
const mode = executionConfig.mode;
const slippageBps = executionConfig.slippageBps;
const executableSymbol = await resolveExecutableMarketSymbol(config);
const orderSymbol = resolveHyperliquidSymbol(
executableSymbol,
executionConfig.symbol ?? config.marketSymbol,
);
const isSpot = isHyperliquidSpotSymbol(orderSymbol);
const baseSymbol = normalizeHyperliquidBaseSymbol(orderSymbol);
const pair = resolveHyperliquidPair(orderSymbol);
const leverageMode = resolveHyperliquidLeverageMode(orderSymbol);
const leverage = executionConfig.leverage;
try {
const chain = resolveHyperliquidChainConfig(environment).chain;
const ctx = await wallet({ chain });
if (isSpot && orderSymbol.startsWith("@")) {
throw new Error("spot execution requires BASE-QUOTE or BASE/QUOTE symbols (no @ ids).");
}
if (isSpot && typeof leverage === "number" && Number.isFinite(leverage)) {
throw new Error("leverage is not supported for spot markets.");
}
const clearing = isSpot
? await fetchHyperliquidSpotClearinghouseState({
environment,
user: ctx.address as `0x${string}`,
})
: await fetchHyperliquidClearinghouseState({
environment,
walletAddress: ctx.address as `0x${string}`,
});
const currentSize = isSpot
? readHyperliquidSpotBalanceSize(clearing, orderSymbol)
: readHyperliquidPerpPositionSize(clearing, orderSymbol, { prefixMatch: true });
const accountValue = isSpot
? await fetchHyperliquidSpotAccountValue({
environment,
balances: (clearing as { balances?: unknown }).balances,
})
: readHyperliquidAccountValue(clearing);
const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({
config,
execution: executionConfig,
accountValue,
currentPrice,
});
const plan = planHyperliquidTrade({
signal: tradeSignal,
mode,
currentSize,
targetSize,
});
const orderResponses: HyperliquidOrderResponse[] = [];
let sizeDecimals: number | undefined;
if (!isSpot && typeof leverage === "number" && Number.isFinite(leverage)) {
await updateHyperliquidLeverage({
wallet: ctx as WalletFullContext,
environment,
input: {
symbol: orderSymbol,
leverageMode,
leverage,
},
});
}
if (plan) {
const price = formatHyperliquidMarketablePrice({
mid: currentPrice,
side: plan.side,
slippageBps,
});
sizeDecimals = await fetchHyperliquidSizeDecimals({
symbol: orderSymbol,
environment,
});
const size = formatHyperliquidOrderSize(plan.size, sizeDecimals);
if (size === "0") {
throw new Error(`Order size too small for ${orderSymbol} (szDecimals=${sizeDecimals}).`);
}
const response = await placeHyperliquidOrder({
wallet: ctx as WalletFullContext,
environment,
orders: [
{
symbol: orderSymbol,
side: plan.side,
price,
size,
tif: "FrontendMarket",
reduceOnly: plan.reduceOnly,
},
],
});
const orderIds = resolveOrderIds([response]);
const orderRef =
orderIds.cloids[0] ?? orderIds.oids[0] ?? `polymarket-market-watcher-${Date.now()}`;
const marketIdentity = buildHyperliquidMarketIdentity({
environment,
symbol: pair ?? orderSymbol,
rawSymbol: orderSymbol,
isSpot,
base: baseSymbol ?? null,
});
if (!marketIdentity) {
throw new Error("Unable to resolve market identity for order.");
}
await store({
source: "hyperliquid",
ref: orderRef,
status: "submitted",
walletAddress: ctx.address,
action: "order",
notional: size,
network: environment === "mainnet" ? "hyperliquid" : "hyperliquid-testnet",
market: marketIdentity,
metadata: {
symbol: baseSymbol,
pair: pair ?? undefined,
side: plan.side,
price,
size,
reduceOnly: plan.reduceOnly,
...(typeof leverage === "number" ? { leverage } : {}),
environment,
cloid: orderIds.cloids[0] ?? null,
orderIds,
orderResponse: response,
strategy: "polymarket-market-watcher",
},
});
orderResponses.push(response);
}
return {
ok: true,
execution: {
enabled: true,
signal: tradeSignal,
action: plan ? "order" : "noop",
environment,
mode,
symbol: baseSymbol,
pair,
leverageMode,
...(typeof leverage === "number" ? { leverage } : {}),
slippageBps,
sizeDecimals,
budgetUsd,
targetSize,
currentSize,
orderIds: resolveOrderIds(orderResponses),
orderResponses: orderResponses.length ? orderResponses : undefined,
},
};
} catch (error) {
const message = error instanceof Error ? error.message : "execution_failed";
const errorDetail = resolveHyperliquidErrorDetail(error);
return {
ok: false,
error: message,
execution: {
enabled: true,
signal: tradeSignal,
action: "error",
environment,
mode,
symbol: baseSymbol,
leverageMode,
...(typeof leverage === "number" ? { leverage } : {}),
error: message,
...(errorDetail ? { errorDetail } : {}),
},
};
}
}
function resolveOrderIds(responses: HyperliquidOrderResponse[]): { cloids: string[]; oids: string[] } {
const cloids = new Set<string>();
const oids = new Set<string>();
for (const response of responses) {
const statuses = response.response?.data?.statuses;
if (!Array.isArray(statuses)) continue;
for (const status of statuses) {
if (typeof status === "string") continue;
if ("resting" in status) {
if (status.resting.cloid) cloids.add(status.resting.cloid);
oids.add(String(status.resting.oid));
continue;
}
if ("filled" in status) {
if (status.filled.cloid) cloids.add(status.filled.cloid);
oids.add(String(status.filled.oid));
}
}
}
return {
cloids: Array.from(cloids),
oids: Array.from(oids),
};
}