// Trading Automation Harness — Type definitions // The API contract that automation scripts code against import type { HyperliquidClient } from '../core/client.js'; // ── Factory function ──────────────────────────────────────────────── /** What an automation .ts file exports */ export type AutomationFactory = (api: AutomationAPI) => void | Promise; /** Runtime-enforced policy exported by every automation module. */ export type AutomationGuardrails = | ReadOnlyAutomationGuardrails | TradingAutomationGuardrails; export interface ReadOnlyAutomationGuardrails { /** Read-only automations cannot call any client write method. */ mode: 'read-only'; } export interface TradingAutomationGuardrails { mode: 'trading'; /** Canonical markets this automation may trade: `ETH`, `xyz:CL`, `spot:HYPE`, `#1230`. */ allowedMarkets: string[]; /** Maximum USD notional for one submitted order. */ maxOrderUsd: number; /** Maximum absolute USD exposure in any one market after an order. */ maxPositionUsd: number; /** Maximum absolute USD exposure across the account after an order. */ maxTotalExposureUsd: number; /** Maximum leverage the automation may request or use when increasing perp exposure. */ maxLeverage: number; /** Maximum account margin utilization allowed after a risk-increasing perp order. */ maxMarginUsedPct: number; /** Maximum account-wide open orders after this automation submits an order. */ maxOpenOrders: number; /** Maximum risk-increasing order submissions in a rolling 60-second window. */ maxOrdersPerMinute: number; /** Maximum slippage passed to market-order helpers. */ maxSlippageBps: number; /** Whether market-order helpers may be used. */ allowMarketOrders: boolean; /** Whether `cancelAll()` without a market or an armed `scheduleCancel()` is allowed. */ allowAccountWideCancel: boolean; } export interface AutomationGuardrailContext { /** Persisted automation state with current `--set` overrides applied. */ config: Readonly>; } /** Guardrails may be static or derived from startup config, but the result is always validated. */ export type AutomationGuardrailsExport = | AutomationGuardrails | ((context: AutomationGuardrailContext) => AutomationGuardrails); export interface LoadedAutomation { factory: AutomationFactory; guardrails: AutomationGuardrails; } /** Config field descriptor for example automations */ export interface AutomationConfigField { type: 'string' | 'number' | 'boolean'; description: string; default: unknown; required?: boolean; } /** Config metadata exported by example automations as `export const config` */ export interface AutomationConfig { description: string; fields: Record; } // ── Event system ──────────────────────────────────────────────────── export type AutomationEventType = | 'tick' | 'price_change' | 'funding_update' | 'position_opened' | 'position_closed' | 'position_changed' | 'pnl_threshold' | 'margin_warning' | 'order_filled' | 'order_update' | 'liquidation'; export interface AutomationEventPayloads { tick: { timestamp: number; pollCount: number }; price_change: { coin: string; oldPrice: number; newPrice: number; changePct: number }; funding_update: { coin: string; fundingRate: number; annualized: number; premium: number }; position_opened: { coin: string; side: 'long' | 'short'; size: number; entryPrice: number }; position_closed: { coin: string; previousSize: number; entryPrice: number }; position_changed: { coin: string; oldSize: number; newSize: number; entryPrice: number }; pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number }; margin_warning: { marginUsedPct: number; equity: number; marginUsed: number }; /** * Fires on every trade fill — partial and terminal — sourced from the * Hyperliquid `userFills` WS stream. `size` is the fill delta (NOT remaining * size of the order). `fee` and `closedPnl` are in USD (converted from the * raw fee denomination using `price` when `feeToken !== "USDC"`, which * happens on spot buys where the fee is charged in the received asset). * `feeToken` is the original denomination so consumers can identify legs * that won't carry a builder fee (Hyperliquid only charges builder fees on * USDC-denominated trades). `crossed` is true when this side was the taker. * Fee/pnl/crossed/feeToken are optional so older consumers that only read * coin/oid/side/size/price keep working. */ order_filled: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number; fee?: number; feeToken?: string; closedPnl?: number; crossed?: boolean; }; /** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */ order_update: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number; origSize: number; status: string; statusTimestamp: number; }; /** Liquidation event via WebSocket — only source for liquidation alerts */ liquidation: { lid: number; liquidator: string; liquidatedUser: string; liquidatedNtlPos: number; liquidatedAccountValue: number; }; } export type AutomationEventHandler = (payload: AutomationEventPayloads[E]) => void | Promise; // ── State & logging ───────────────────────────────────────────────── export interface AutomationState { get(key: string, defaultValue?: T): T | undefined; set(key: string, value: T): void; delete(key: string): void; clear(): void; } export interface AutomationLogger { info(message: string): void; warn(message: string): void; error(message: string): void; debug(message: string): void; } export interface AutomationAudit { /** Record a custom audit note for later reporting. */ record(kind: string, payload?: unknown): void; /** Record a numeric metric with optional dimensions/tags. */ metric(name: string, value: number, tags?: Record): void; } /** * Hook for external monitoring packages (e.g. `openbroker-monitoring`) to * receive audit events. The runtime fans out every note, metric, and * audited write-method call to every observer it has loaded. * * Observers are auto-loaded via convention dynamic-import — the runtime * tries `await import('openbroker-monitoring')` at startup and uses the * default export (a factory returning an observer or null). */ export interface AutomationAuditObserver { onNote?(kind: string, payload?: unknown): void; onMetric?(name: string, value: number, tags?: Record): void; onAgentAction?( action: string, status: 'success' | 'error', details: Record, txHash?: string, ): void; } // ── Publish (webhook) ─────────────────────────────────────────────── export interface PublishOptions { /** Human-readable name for the hook (appears in logs). Default: "ob-auto-" */ name?: string; /** Wake mode: "now" triggers immediate agent turn, "next-heartbeat" queues. Default: "now" */ wakeMode?: 'now' | 'next-heartbeat'; /** Whether to deliver the agent response to messaging channels. Default: true */ deliver?: boolean; /** Target channel (e.g. "slack", "telegram", "last"). Default: agent decides */ channel?: string; } // ── Core API ──────────────────────────────────────────────────────── export interface AutomationAPI { /** Full Hyperliquid client (42+ methods) */ client: HyperliquidClient; /** Convenience utilities from core */ utils: { roundPrice: (price: number, szDecimals: number, isSpot?: boolean) => string; roundSize: (size: number, szDecimals: number) => string; sleep: (ms: number) => Promise; normalizeCoin: (coin: string) => string; formatUsd: (amount: number | string) => string; formatPercent: (value: number | string, decimals?: number) => string; annualizeFundingRate: (hourlyRate: number | string) => number; }; /** Subscribe to a market/account event */ on(event: E, handler: AutomationEventHandler): void; /** Run a handler on its own recurring scheduler, independent of REST reconciliation. */ every(intervalMs: number, handler: () => void | Promise): void; /** Called after all handlers are registered and polling begins */ onStart(handler: () => void | Promise): void; /** Called when automation is stopping (SIGINT, manual stop). Use for cleanup. */ onStop(handler: () => void | Promise): void; /** Called when a handler throws. The error is already logged — use this for recovery logic. */ onError(handler: (error: Error) => void | Promise): void; /** * Publish a message to the OpenClaw agent via webhook. * Sends to POST /hooks/agent on the local gateway, triggering an agent turn. * The agent receives the message and can act on it (notify user, trade, etc.). * * @param message — The message string the agent will receive * @param options — Optional: name, wakeMode, deliver, channel * @returns true if delivered, false if webhook is not configured */ publish(message: string, options?: PublishOptions): Promise; /** Persisted key-value state (~/.openbroker/state/.json) */ state: AutomationState; /** Structured logger */ log: AutomationLogger; /** Local audit trail persisted to ~/.openbroker/automation-audit.sqlite */ audit: AutomationAudit; /** Unique automation ID (derived from filename or --id flag) */ id: string; /** True if running in --dry mode (write methods are intercepted) */ dryRun: boolean; /** Validated policy currently enforced by the runtime client proxy. */ guardrails: Readonly; } // ── Runtime internals ─────────────────────────────────────────────── export interface AutomationSnapshot { prices: Map; positions: Map; openOrderIds: Set; equity: number; marginUsed: number; marginUsedPct: number; fundingRates: Map; timestamp: number; } export interface PositionSnapshot { coin: string; size: number; entryPrice: number; positionValue: number; unrealizedPnl: number; liquidationPx: number | null; leverage: number; marginUsed: number; } export interface ScheduledTask { intervalMs: number; handler: () => void | Promise; lastRun: number; running?: boolean; } export interface RunningAutomation { id: string; scriptPath: string; startedAt: Date; pollCount: number; eventsEmitted: number; dryRun: boolean; /** * Stop the automation. * @param opts.persist If false, keep the entry in the file registry so it * restarts when the gateway comes back up. Default: true (fully remove). */ stop: (opts?: { persist?: boolean }) => Promise; }