openpondai/agents/dca-agent
dca-agent
typescript
import {
clampHyperliquidInt,
parseHyperliquidJson,
resolveHyperliquidDcaSymbolEntries,
} from "opentool/adapters/hyperliquid";
import {
CONFIG_ENV,
DEFAULT_AMOUNT_USD,
DEFAULT_ASSET,
DEFAULT_COUNT_BACK,
DEFAULT_EXECUTION_ENV,
DEFAULT_RESOLUTION,
DEFAULT_SCHEDULE_CRON,
DEFAULT_SLIPPAGE_BPS,
TEMPLATE_CONFIG_DEFAULTS,
TEMPLATE_CONFIG_ENV_VAR,
TEMPLATE_CONFIG_VERSION,
} from "./defaults";
import { configSchema, TEMPLATE_CONFIG_SCHEMA } from "./schema";
import type {
DcaAgentConfig,
ScheduleConfig,
} from "./types";
export const DCA_AGENT_TEMPLATE_CONFIG = {
version: TEMPLATE_CONFIG_VERSION,
schema: TEMPLATE_CONFIG_SCHEMA,
defaults: TEMPLATE_CONFIG_DEFAULTS,
envVar: TEMPLATE_CONFIG_ENV_VAR,
};
function sanitizeConfigCandidate(value: unknown): unknown {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return value;
}
const record = value as Record<string, unknown>;
const execution =
record.execution && typeof record.execution === "object" && !Array.isArray(record.execution)
? (record.execution as Record<string, unknown>)
: null;
if (!execution) {
return value;
}
const slippageBps = execution.slippageBps;
if (typeof slippageBps !== "number" || !Number.isFinite(slippageBps)) {
return value;
}
return {
...record,
execution: {
...execution,
// Keep starter-generated execution config within schema limits so the
// rest of the template payload still parses.
slippageBps: Math.max(0, Math.min(500, slippageBps)),
},
};
}
export function readConfig(): DcaAgentConfig {
const envConfig = sanitizeConfigCandidate(
parseHyperliquidJson(process.env[CONFIG_ENV] ?? null),
);
const parsed = configSchema.safeParse(envConfig);
const input = parsed.success ? parsed.data : {};
const amountUsd = input.amountUsd ?? DEFAULT_AMOUNT_USD;
const base = {
platform: "hyperliquid" as const,
allocationMode: "fixed" as const,
amountUsd,
schedule: {
cron: input.schedule?.cron ?? DEFAULT_SCHEDULE_CRON,
enabled: input.schedule?.enabled ?? false,
notifyEmail: input.schedule?.notifyEmail ?? false,
},
resolution: input.resolution ?? DEFAULT_RESOLUTION,
countBack: input.countBack ?? DEFAULT_COUNT_BACK,
};
const execution =
input.execution
? {
enabled: input.execution.enabled ?? false,
environment: input.execution.environment ?? DEFAULT_EXECUTION_ENV,
slippageBps: clampHyperliquidInt(
input.execution.slippageBps,
0,
500,
DEFAULT_SLIPPAGE_BPS,
),
...(input.execution.symbol ? { symbol: input.execution.symbol } : {}),
...(input.execution.leverage
? { leverage: input.execution.leverage }
: {}),
}
: undefined;
const asset = (input.asset ?? DEFAULT_ASSET).toUpperCase();
const symbols = resolveHyperliquidDcaSymbolEntries(
input.dca?.symbols as Array<{ symbol: string; weight?: number } | string> | undefined,
asset,
);
return {
...base,
signalType: "dca",
asset,
dca: {
preset: input.dca?.preset ?? asset,
symbols,
slippageBps: input.dca?.slippageBps ?? DEFAULT_SLIPPAGE_BPS,
},
...(execution ? { execution } : {}),
};
}
export function resolveScheduleConfig(config: DcaAgentConfig): ScheduleConfig {
return config.schedule;
}
export function resolveProfileAssetSymbols(config: DcaAgentConfig): string[] {
const symbols = config.dca?.symbols?.map((entry) => entry.symbol) ?? [];
const normalized = symbols
.filter((symbol): symbol is string => typeof symbol === "string")
.map((symbol) => symbol.trim())
.filter((symbol) => symbol.length > 0);
if (normalized.length > 0) {
return Array.from(new Set(normalized));
}
const fallback = config.execution?.symbol ?? config.asset ?? DEFAULT_ASSET;
const normalizedFallback = fallback.trim();
return normalizedFallback ? [normalizedFallback] : [];
}
export function resolveProfileAssets(config: DcaAgentConfig): Array<{
venue: "hyperliquid";
chain: "hyperliquid" | "hyperliquid-testnet";
assetSymbols: string[];
pair?: string;
leverage?: number;
}> {
const environment = (config.execution?.environment ?? "mainnet") as "mainnet" | "testnet";
const chain =
environment === "testnet" ? "hyperliquid-testnet" : "hyperliquid";
const symbols = resolveProfileAssetSymbols(config)
.map((symbol) => symbol.trim())
.filter((symbol) => symbol.length > 0);
if (symbols.length === 0) {
return [];
}
return [
{
venue: "hyperliquid",
chain,
assetSymbols: symbols,
...(typeof config.execution?.leverage === "number"
? { leverage: config.execution.leverage }
: {}),
},
];
}