openpondai/agents/twap-bot
OpenTool app
typescript
import {
buildHyperliquidMarketIdentity,
extractHyperliquidOrderIds,
fetchHyperliquidResolvedMarketDescriptor,
placeHyperliquidTwapOrder,
resolveHyperliquidLeverageMode,
updateHyperliquidLeverage,
type HyperliquidEnvironment,
} from "opentool/adapters/hyperliquid";
import { store } from "opentool/store";
import { wallet } from "opentool/wallet";
import type { WalletFullContext } from "opentool/wallet";
import { normalizeMarketSymbol, type TwapBotConfig } from "../config";
import { isSpotSymbol, readObservedPrice } from "./market";
export type TwapBotResult = {
ok: boolean;
simulated: boolean;
environment: HyperliquidEnvironment;
asset: string;
marketSymbol: string;
side: "buy" | "sell";
amountUsd: number;
minutes: number;
randomize: boolean;
price: number;
size: number;
orderIds?: {
cloids: string[];
oids: string[];
};
orderResponses?: unknown[];
response?: unknown;
};
export async function runTwapBot(config: TwapBotConfig): Promise<TwapBotResult> {
const environment = config.execution?.environment ?? "mainnet";
const marketSymbol = normalizeMarketSymbol(config.marketSymbol || config.asset);
const marketDescriptor = await fetchHyperliquidResolvedMarketDescriptor({
environment,
symbol: marketSymbol,
});
const orderSymbol = marketDescriptor.orderSymbol;
const isSpot =
marketDescriptor.kind === "spot" || marketDescriptor.kind === "spotIndex";
const price = await readObservedPrice({
environment,
symbol: marketSymbol,
});
const size = config.amountUsd / price;
if (!Number.isFinite(size) || size <= 0) {
throw new Error(`Unable to derive a TWAP size for ${marketSymbol}.`);
}
let response: unknown | undefined;
if (config.execution?.enabled) {
const walletContext = (await wallet()) as WalletFullContext;
if (
!isSpot &&
typeof config.execution?.leverage === "number"
) {
const leverageMode = resolveHyperliquidLeverageMode(orderSymbol);
await updateHyperliquidLeverage({
wallet: walletContext,
environment,
input: {
symbol: orderSymbol,
leverageMode,
leverage: config.execution.leverage,
},
});
}
response = await placeHyperliquidTwapOrder({
wallet: walletContext,
environment,
twap: {
symbol: orderSymbol,
side: config.twap.side,
size,
minutes: config.twap.minutes,
randomize: config.twap.randomize,
reduceOnly: false,
},
});
const orderIds = extractHyperliquidOrderIds([
response as {
response?: {
data?: {
statuses?: Array<Record<string, unknown>>;
};
};
},
]);
const marketIdentity = buildHyperliquidMarketIdentity({
environment,
symbol: marketDescriptor.pair ?? orderSymbol,
rawSymbol: orderSymbol,
isSpot,
base: marketDescriptor.base ?? config.asset,
quote: marketDescriptor.quote,
});
if (marketIdentity) {
await store({
source: "hyperliquid",
ref:
orderIds.cloids[0] ??
orderIds.oids[0] ??
`twap-bot-${Date.now()}-${marketSymbol}`,
status: "submitted",
walletAddress: walletContext.address,
action: "order",
notional: String(size),
network:
environment === "mainnet" ? "hyperliquid" : "hyperliquid-testnet",
market: marketIdentity,
metadata: {
strategy: "twap-bot",
symbol: marketSymbol,
side: config.twap.side,
minutes: config.twap.minutes,
randomize: config.twap.randomize,
amountUsd: config.amountUsd,
price,
size,
cloid: orderIds.cloids[0] ?? null,
orderIds,
response,
orderResponses: [response],
},
});
}
}
const result: TwapBotResult = {
ok: true,
simulated: !config.execution?.enabled,
environment,
asset: config.asset,
marketSymbol,
side: config.twap.side,
amountUsd: config.amountUsd,
minutes: config.twap.minutes,
randomize: config.twap.randomize,
price,
size,
...(response
? {
orderIds: extractHyperliquidOrderIds([
response as {
response?: {
data?: {
statuses?: Array<Record<string, unknown>>;
};
};
},
]),
orderResponses: [response],
}
: {}),
...(response ? { response } : {}),
};
await store({
source: "twap-bot",
ref: `twap-bot-${Date.now()}`,
status: "info",
action: "signal",
metadata: result,
});
return result;
}