openpondai/agents/signal-bot
OpenTool app
typescript
import { store } from "opentool/store";
import { wallet } from "opentool/wallet";
import type { WalletFullContext } from "opentool/wallet";
import {
buildHyperliquidMarketIdentity,
fetchHyperliquidClearinghouseState,
fetchHyperliquidDexMetaAndAssetCtxs,
fetchHyperliquidSizeDecimals,
fetchHyperliquidSpotClearinghouseState,
fetchHyperliquidTickSize,
formatHyperliquidMarketablePrice,
formatHyperliquidOrderSize,
isHyperliquidSpotSymbol,
normalizeHyperliquidBaseSymbol,
normalizeHyperliquidMetaSymbol,
placeHyperliquidOrder,
planHyperliquidTrade,
readHyperliquidPerpPositionSize,
readHyperliquidSpotBalanceSize,
resolveHyperliquidChainConfig,
resolveHyperliquidErrorDetail,
resolveHyperliquidLeverageMode,
resolveHyperliquidPair,
resolveHyperliquidSymbol,
resolveHyperliquidTargetSize,
updateHyperliquidLeverage,
type HyperliquidEnvironment,
type HyperliquidOrderResponse,
} from "opentool/adapters/hyperliquid";
import {
DEFAULT_EXECUTION_ENV,
DEFAULT_EXECUTION_MODE,
DEFAULT_SLIPPAGE_BPS,
type IndicatorType,
type SignalBotConfig,
} from "../config";
import { extractOrderIds } from "./order-utils";
import { buildSignalTargetSizeConfig, buildSignalTargetSizeExecution } from "./target-size";
async function readOrderSizeDecimals(params: {
environment: HyperliquidEnvironment;
symbol: string;
}) {
if (!params.symbol.includes(":")) {
return fetchHyperliquidSizeDecimals(params);
}
const [dexRaw] = params.symbol.split(":");
const dex = dexRaw?.trim().toLowerCase();
if (!dex) {
return fetchHyperliquidSizeDecimals(params);
}
const data = (await fetchHyperliquidDexMetaAndAssetCtxs(
params.environment,
dex,
)) as [{ universe?: Array<{ name?: string; szDecimals?: number | string }> }, Array<Record<string, unknown>>];
const universe = Array.isArray(data?.[0]?.universe) ? data[0].universe : [];
const target = normalizeHyperliquidMetaSymbol(params.symbol).toUpperCase();
const entry =
universe.find(
(item) => normalizeHyperliquidMetaSymbol(item?.name ?? "").toUpperCase() === target,
) ?? null;
const rawDecimals = entry?.szDecimals;
const sizeDecimals =
typeof rawDecimals === "number"
? rawDecimals
: typeof rawDecimals === "string"
? Number.parseFloat(rawDecimals)
: Number.NaN;
if (!Number.isFinite(sizeDecimals)) {
throw new Error(`No size decimals found for ${params.symbol}.`);
}
return sizeDecimals;
}
export async function executePriceSignal(params: {
config: SignalBotConfig;
currentPrice: number;
tradeSignal: "buy" | "sell" | "hold" | "unknown";
indicator: IndicatorType;
}): Promise<{ execution?: Record<string, unknown>; executionError: string | null }> {
const { config, currentPrice, tradeSignal, indicator } = params;
const environment =
(config.execution?.environment ?? DEFAULT_EXECUTION_ENV) as HyperliquidEnvironment;
const mode = config.execution?.mode ?? DEFAULT_EXECUTION_MODE;
const slippageBps = config.execution?.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
const orderSymbol = resolveHyperliquidSymbol(config.asset, config.execution?.symbol);
const isSpot = isHyperliquidSpotSymbol(orderSymbol);
const baseSymbol = normalizeHyperliquidBaseSymbol(orderSymbol);
const pair = resolveHyperliquidPair(orderSymbol);
const leverageMode = resolveHyperliquidLeverageMode(orderSymbol);
const leverage = config.execution?.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 { targetSize, budgetUsd } = resolveHyperliquidTargetSize({
config: buildSignalTargetSizeConfig(config),
execution: buildSignalTargetSizeExecution(config),
accountValue: null,
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 tick = await fetchHyperliquidTickSize({
symbol: orderSymbol,
environment,
});
const resolvedSizeDecimals = await readOrderSizeDecimals({
symbol: orderSymbol,
environment,
});
sizeDecimals = resolvedSizeDecimals;
const price = formatHyperliquidMarketablePrice({
mid: currentPrice,
side: plan.side,
slippageBps,
tick,
szDecimals: resolvedSizeDecimals,
marketType: isSpot ? "spot" : "perp",
});
const size = formatHyperliquidOrderSize(plan.size, resolvedSizeDecimals);
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 = extractOrderIds([response]);
const orderRef =
orderIds.cloids[0] ??
orderIds.oids[0] ??
`signal-bot-${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,
assetSymbols: undefined,
side: plan.side,
price,
size,
reduceOnly: plan.reduceOnly,
...(typeof leverage === "number" ? { leverage } : {}),
environment,
cloid: orderIds.cloids[0] ?? null,
orderIds,
orderResponse: response,
strategy: "signal-bot",
},
});
orderResponses.push(response);
}
return {
executionError: null,
execution: {
enabled: true,
indicator,
signal: tradeSignal,
action: plan ? "order" : "noop",
environment,
mode,
symbol: baseSymbol,
pair,
leverageMode,
...(typeof leverage === "number" ? { leverage } : {}),
slippageBps,
sizeDecimals,
budgetUsd,
targetSize,
currentSize,
orderIds: extractOrderIds(orderResponses),
orderResponses: orderResponses.length ? orderResponses : undefined,
},
};
} catch (error) {
const executionError = error instanceof Error ? error.message : "execution_failed";
const errorDetail = resolveHyperliquidErrorDetail(error);
return {
executionError,
execution: {
enabled: true,
indicator,
signal: tradeSignal,
action: "error",
environment,
mode,
symbol: baseSymbol,
leverageMode,
...(typeof leverage === "number" ? { leverage } : {}),
error: executionError,
...(errorDetail ? { errorDetail } : {}),
},
};
}
}