openpondai/agents/dca-agent
dca-agent
typescript
import { store } from "opentool/store";
import { wallet } from "opentool/wallet";
import type { WalletFullContext } from "opentool/wallet";
import {
buildHyperliquidMarketIdentity,
fetchHyperliquidBars,
fetchHyperliquidDexMetaAndAssetCtxs,
fetchHyperliquidSizeDecimals,
formatHyperliquidMarketablePrice,
formatHyperliquidOrderSize,
isHyperliquidSpotSymbol,
normalizeHyperliquidBaseSymbol,
normalizeHyperliquidDcaEntries,
normalizeHyperliquidMetaSymbol,
placeHyperliquidOrder,
resolveHyperliquidBudgetUsd,
resolveHyperliquidChainConfig,
resolveHyperliquidErrorDetail,
resolveHyperliquidLeverageMode,
resolveHyperliquidPair,
resolveHyperliquidSymbol,
updateHyperliquidLeverage,
type HyperliquidEnvironment,
type HyperliquidOrderResponse,
} from "opentool/adapters/hyperliquid";
import {
DEFAULT_ASSET,
DEFAULT_EXECUTION_ENV,
DEFAULT_SLIPPAGE_BPS,
type DcaAgentConfig,
} from "../config";
import { extractOrderIds } from "./order-utils";
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 executeDcaOrders(params: {
config: DcaAgentConfig;
dca: NonNullable<DcaAgentConfig["dca"]>;
}): Promise<{ execution?: Record<string, unknown>; executionError: string | null }> {
const { config, dca } = params;
const environment =
(config.execution?.environment ?? DEFAULT_EXECUTION_ENV) as HyperliquidEnvironment;
const slippageBps = config.execution?.slippageBps ?? dca.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
const leverage = config.execution?.leverage;
const entries = normalizeHyperliquidDcaEntries({
entries: config.dca?.symbols,
fallbackSymbol: config.asset ?? DEFAULT_ASSET,
});
const hasSpotEntries = entries.some((entry) =>
isHyperliquidSpotSymbol(resolveHyperliquidSymbol(entry.symbol)),
);
try {
const chain = resolveHyperliquidChainConfig(environment).chain;
const ctx = await wallet({ chain });
if (hasSpotEntries && typeof leverage === "number" && Number.isFinite(leverage)) {
throw new Error("leverage is not supported for spot markets.");
}
const budgetUsd = resolveHyperliquidBudgetUsd({ config, accountValue: null });
if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) {
throw new Error("DCA budget must be greater than zero.");
}
const orderResponses: HyperliquidOrderResponse[] = [];
const results: Array<Record<string, unknown>> = [];
const errors: Array<Record<string, unknown>> = [];
for (const entry of entries) {
const symbolBudget = budgetUsd * entry.normalizedWeight;
if (!Number.isFinite(symbolBudget) || symbolBudget <= 0) {
errors.push({ symbol: entry.symbol, error: "budget too small" });
continue;
}
const bars = await fetchHyperliquidBars({
symbol: entry.symbol,
resolution: config.resolution,
countBack: config.countBack,
});
if (bars.length === 0) {
errors.push({ symbol: entry.symbol, error: "no price data" });
continue;
}
const currentPrice = bars[bars.length - 1]!.close;
const orderSymbol = resolveHyperliquidSymbol(entry.symbol);
const isSpot = isHyperliquidSpotSymbol(orderSymbol);
if (isSpot && orderSymbol.startsWith("@")) {
errors.push({ symbol: entry.symbol, error: "spot requires BASE/QUOTE" });
continue;
}
const baseSymbol = normalizeHyperliquidBaseSymbol(orderSymbol);
const pair = resolveHyperliquidPair(orderSymbol);
const leverageMode = resolveHyperliquidLeverageMode(orderSymbol);
if (!isSpot && typeof leverage === "number" && Number.isFinite(leverage)) {
await updateHyperliquidLeverage({
wallet: ctx as WalletFullContext,
environment,
input: {
symbol: orderSymbol,
leverageMode,
leverage,
},
});
}
const sizeDecimals = await readOrderSizeDecimals({ symbol: orderSymbol, environment });
const price = formatHyperliquidMarketablePrice({
mid: currentPrice,
side: "buy",
slippageBps,
szDecimals: sizeDecimals,
marketType: isSpot ? "spot" : "perp",
});
const size = formatHyperliquidOrderSize(symbolBudget / currentPrice, sizeDecimals);
if (size === "0") {
errors.push({
symbol: entry.symbol,
error: `Order size too small (szDecimals=${sizeDecimals}).`,
});
continue;
}
const response = await placeHyperliquidOrder({
wallet: ctx as WalletFullContext,
environment,
orders: [
{
symbol: orderSymbol,
side: "buy",
price,
size,
tif: "FrontendMarket",
reduceOnly: false,
},
],
});
const orderIds = extractOrderIds([response]);
const orderRef =
orderIds.cloids[0] ??
orderIds.oids[0] ??
`dca-agent-${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,
market: orderSymbol,
pair: pair ?? undefined,
assetSymbols: entries.map((entry) => entry.symbol),
side: "buy",
price,
size,
reduceOnly: false,
...(typeof leverage === "number" ? { leverage } : {}),
environment,
weight: entry.weight,
budgetUsd: symbolBudget,
cloid: orderIds.cloids[0] ?? null,
orderIds,
orderResponse: response,
strategy: "dca-agent",
},
});
orderResponses.push(response);
results.push({
symbol: entry.symbol,
budgetUsd: symbolBudget,
orderSymbol,
size,
price,
orderIds,
});
}
return {
executionError: null,
execution: {
enabled: true,
environment,
mode: "long-only",
slippageBps,
budgetUsd,
orders: results,
errors: errors.length > 0 ? errors : undefined,
orderIds: extractOrderIds(orderResponses),
},
};
} catch (error) {
const executionError = error instanceof Error ? error.message : "execution_failed";
const errorDetail = resolveHyperliquidErrorDetail(error);
return {
executionError,
execution: {
enabled: true,
environment,
mode: "long-only",
...(typeof leverage === "number" ? { leverage } : {}),
error: executionError,
...(errorDetail ? { errorDetail } : {}),
},
};
}
}