1Branch0Tags
GL
glucryptoRefresh package-lock for opentool 0.19.5
01acdef14 hours ago17Commits
typescript
import { buildHyperliquidProfileAssets, normalizeHyperliquidBaseSymbol } from "opentool/adapters/hyperliquid"; import { z } from "zod"; const CONFIG_ENV = "OPENTOOL_PUBLIC_NEWS_BOT_CONFIG"; const TEMPLATE_CONFIG_ENV_VAR = CONFIG_ENV; const TEMPLATE_CONFIG_VERSION = 3; const DEFAULT_ASSET = "BTC"; const DEFAULT_AMOUNT_USD = 200; const DEFAULT_PERCENT_OF_EQUITY = 2; const DEFAULT_MAX_PERCENT_OF_EQUITY = 10; export const DEFAULT_RESOLUTION = "60" as const; const DEFAULT_COUNT_BACK = 240; export const DEFAULT_EXECUTION_ENV = "mainnet" as const; export const DEFAULT_EXECUTION_MODE = "long-only" as const; export const DEFAULT_SLIPPAGE_BPS = 50; const scheduleSchema = z .object({ cron: z.string().min(1), enabled: z.boolean().optional(), notifyEmail: z.boolean().optional(), }) .strict(); const eventGateSchema = z .object({ mode: z.literal("event"), minConfidence: z.number().min(0).max(1).optional(), maxDataAgeMs: z.number().int().positive().optional(), minIndependentSources: z.number().int().min(1).optional(), minTierASources: z.number().int().min(0).optional(), requireTriggerPassed: z.boolean().optional(), onBlocked: z.enum(["skip", "pause"]).optional(), }) .strict(); const propositionGateSchema = z .object({ mode: z.literal("proposition"), expectedAnswer: z.enum(["yes", "no", "unclear"]).optional(), minConfidence: z.number().min(0).max(1).optional(), maxDataAgeMs: z.number().int().positive().optional(), requireResolvedEvent: z.boolean().optional(), onBlocked: z.enum(["skip", "pause"]).optional(), }) .strict(); const predictionMarketSignalSchema = z .object({ preferredOutcome: z.enum(["yes", "no"]), minProbability: z.number().min(0).max(1), minLiquidity: z.number().nonnegative().optional(), action: z.enum(["buy", "sell"]).optional(), onNoMatch: z.enum(["hold", "unknown"]).optional(), }) .strict(); const executionSchema = z .object({ enabled: z.boolean().optional(), environment: z.enum(["testnet", "mainnet"]).optional(), symbol: z.string().min(1).optional(), mode: z.enum(["long-only", "long-short"]).optional(), size: z.number().positive().optional(), leverage: z.number().positive().optional(), slippageBps: z.number().min(0).max(500).optional(), }) .strict(); const tradePolicySchema = z .object({ eventEntrySignals: z.array(z.literal("escalation")).optional(), eventExitSignals: z.array(z.enum(["de_escalation", "resolved", "contradiction"])).optional(), propositionEntryAnswers: z.array(z.literal("yes")).optional(), propositionExitAnswers: z.array(z.enum(["no", "unclear"])).optional(), blockAction: z.enum(["hold", "close"]).optional(), }) .strict(); const baseConfigSchema = z .object({ configVersion: z.number().int().optional(), asset: z.string().min(1).optional(), marketSymbol: z.string().min(1).optional(), includePredictionMarkets: z.boolean().optional(), ingestOnRequest: z.boolean().optional(), maxAgeHours: z.number().int().min(1).max(24 * 90).optional(), allocationMode: z.enum(["fixed", "percent_equity"]).optional(), amountUsd: z.number().positive().optional(), percentOfEquity: z.number().positive().optional(), maxPercentOfEquity: z.number().positive().optional(), schedule: scheduleSchema.optional(), resolution: z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]).optional(), countBack: z.number().int().min(50).max(5000).optional(), execution: executionSchema.optional(), tradePolicy: tradePolicySchema.optional(), predictionMarketSignal: predictionMarketSignalSchema.optional(), }) .strict(); const eventConfigSchema = baseConfigSchema .extend({ mode: z.literal("event"), query: z.string().min(1).optional(), eventKey: z.string().min(1).optional(), gate: eventGateSchema.optional(), }) .refine((value) => Boolean(value.query || value.eventKey), { message: "query or eventKey is required", path: ["query"], }); const propositionConfigSchema = baseConfigSchema.extend({ mode: z.literal("proposition"), question: z.string().min(3), query: z.string().min(1).optional(), eventKey: z.string().min(1).optional(), propositionType: z.string().min(1).max(96).optional(), candidateLimit: z.number().int().min(1).max(10).optional(), gate: propositionGateSchema.optional(), }); export const simpleNewsBotConfigSchema = z.discriminatedUnion("mode", [ eventConfigSchema, propositionConfigSchema, ]); const gateOverrideSchema = z .object({ mode: z.enum(["event", "proposition"]).optional(), expectedAnswer: z.enum(["yes", "no", "unclear"]).optional(), minConfidence: z.number().min(0).max(1).optional(), maxDataAgeMs: z.number().int().positive().optional(), minIndependentSources: z.number().int().min(1).optional(), minTierASources: z.number().int().min(0).optional(), requireTriggerPassed: z.boolean().optional(), requireResolvedEvent: z.boolean().optional(), onBlocked: z.enum(["skip", "pause"]).optional(), }) .strict(); export const backtestSchema = z .object({ startDate: z.string().min(1), endDate: z.string().min(1).optional(), stepHours: z.number().int().min(1).max(168).optional(), }) .strict(); export const requestOverrideSchema = z .object({ mode: z.enum(["event", "proposition"]).optional(), question: z.string().min(3).optional(), query: z.string().min(1).optional(), eventKey: z.string().min(1).optional(), propositionType: z.string().min(1).max(96).optional(), asset: z.string().min(1).optional(), marketSymbol: z.string().min(1).optional(), includePredictionMarkets: z.boolean().optional(), ingestOnRequest: z.boolean().optional(), maxAgeHours: z.number().int().min(1).max(24 * 90).optional(), allocationMode: z.enum(["fixed", "percent_equity"]).optional(), amountUsd: z.number().positive().optional(), percentOfEquity: z.number().positive().optional(), maxPercentOfEquity: z.number().positive().optional(), candidateLimit: z.number().int().min(1).max(10).optional(), resolution: z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]).optional(), countBack: z.number().int().min(50).max(5000).optional(), asOf: z.string().datetime().optional(), gate: gateOverrideSchema.optional(), execution: executionSchema.optional(), tradePolicy: tradePolicySchema.optional(), predictionMarketSignal: predictionMarketSignalSchema.optional(), backtest: backtestSchema.optional(), }) .strict(); type ParsedSimpleNewsBotConfig = z.infer<typeof simpleNewsBotConfigSchema>; type ParsedEventSimpleNewsBotConfig = Extract<ParsedSimpleNewsBotConfig, { mode: "event" }>; type ParsedPropositionSimpleNewsBotConfig = Extract<ParsedSimpleNewsBotConfig, { mode: "proposition" }>; export type SimpleNewsBotRequestOverrides = z.infer<typeof requestOverrideSchema>; export type SimpleNewsBotBacktest = z.infer<typeof backtestSchema>; type PartialScheduleConfig = z.infer<typeof scheduleSchema>; type PartialExecutionConfig = z.infer<typeof executionSchema>; type PartialTradePolicyConfig = z.infer<typeof tradePolicySchema>; type EventGateConfig = z.infer<typeof eventGateSchema>; type PropositionGateConfig = z.infer<typeof propositionGateSchema>; export type NewsTradeSignal = "buy" | "sell" | "hold" | "unknown"; export type ScheduleConfig = { cron: string; enabled: boolean; notifyEmail: boolean; }; export type ExecutionConfig = { enabled: boolean; environment: "testnet" | "mainnet"; symbol?: string; mode: "long-only" | "long-short"; size?: number; leverage?: number; slippageBps: number; }; export type TradePolicyConfig = { eventEntrySignals: Array<"escalation">; eventExitSignals: Array<"de_escalation" | "resolved" | "contradiction">; propositionEntryAnswers: Array<"yes">; propositionExitAnswers: Array<"no" | "unclear">; blockAction: "hold" | "close"; }; export type PredictionMarketSignalConfig = z.infer<typeof predictionMarketSignalSchema>; type ResolvedSimpleNewsBotConfigBase = { configVersion: number; asset: string; marketSymbol?: string; includePredictionMarkets: boolean; ingestOnRequest: boolean; maxAgeHours: number; allocationMode: "fixed" | "percent_equity"; amountUsd?: number; percentOfEquity: number; maxPercentOfEquity: number; schedule: ScheduleConfig; resolution: "1" | "5" | "15" | "30" | "60" | "240" | "1D" | "1W"; countBack: number; execution: ExecutionConfig; tradePolicy: TradePolicyConfig; predictionMarketSignal?: PredictionMarketSignalConfig; }; export type SimpleNewsBotConfig = | (ResolvedSimpleNewsBotConfigBase & { mode: "event"; query?: string; eventKey?: string; gate?: EventGateConfig; }) | (ResolvedSimpleNewsBotConfigBase & { mode: "proposition"; question: string; query?: string; eventKey?: string; propositionType?: string; candidateLimit: number; gate?: PropositionGateConfig; }); type EventSimpleNewsBotConfig = Extract<SimpleNewsBotConfig, { mode: "event" }>; type PropositionSimpleNewsBotConfig = Extract<SimpleNewsBotConfig, { mode: "proposition" }>; const DEFAULT_SCHEDULE: ScheduleConfig = { cron: "0 * * * *", enabled: false, notifyEmail: false, }; const DEFAULT_TRADE_POLICY: TradePolicyConfig = { eventEntrySignals: ["escalation"], eventExitSignals: ["de_escalation", "resolved", "contradiction"], propositionEntryAnswers: ["yes"], propositionExitAnswers: ["no", "unclear"], blockAction: "hold", }; const TEMPLATE_CONFIG_DEFAULTS: PropositionSimpleNewsBotConfig = { configVersion: TEMPLATE_CONFIG_VERSION, mode: "proposition", question: "Is the US still at war with Iran?", asset: DEFAULT_ASSET, includePredictionMarkets: true, ingestOnRequest: false, maxAgeHours: 24 * 30, allocationMode: "fixed", amountUsd: DEFAULT_AMOUNT_USD, percentOfEquity: DEFAULT_PERCENT_OF_EQUITY, maxPercentOfEquity: DEFAULT_MAX_PERCENT_OF_EQUITY, candidateLimit: 4, gate: { mode: "proposition", expectedAnswer: "yes", minConfidence: 0.6, onBlocked: "skip", }, schedule: DEFAULT_SCHEDULE, resolution: DEFAULT_RESOLUTION, countBack: DEFAULT_COUNT_BACK, execution: { enabled: false, environment: DEFAULT_EXECUTION_ENV, mode: DEFAULT_EXECUTION_MODE, slippageBps: DEFAULT_SLIPPAGE_BPS, }, tradePolicy: DEFAULT_TRADE_POLICY, }; const TEMPLATE_CONFIG_SCHEMA = { type: "object", properties: { configVersion: { type: "number", title: "Config version", description: "Internal version for news-bot defaults.", readOnly: true, "x-hidden": true, }, mode: { type: "string", enum: ["proposition", "event"], title: "Mode", description: "Choose whether the bot reads a proposition question or a canonical event signal.", }, asset: { type: "string", title: "Asset", description: "Default Hyperliquid perp symbol used in strategy output. Use marketSymbol or execution.symbol for exact spot pairs (BASE-QUOTE or BASE/QUOTE) or HIP-3 symbols (dex:BASE).", "x-format": "asset", }, marketSymbol: { type: "string", title: "Exact market symbol", description: "Optional exact Hyperliquid market symbol used for replay and execution. Use BASE-QUOTE or BASE/QUOTE for spot, bare BASE for perps, or dex:BASE for HIP-3.", }, question: { type: "string", title: "Question", description: "Question to send to event-proposition-signal.", "x-visibleIf": { field: "mode", equals: "proposition" }, }, query: { type: "string", title: "Query", description: "Optional search query or event hint used before event resolution.", }, eventKey: { type: "string", title: "Event key", description: "Canonical event key for event-signal or proposition scoping.", }, propositionType: { type: "string", title: "Proposition type", description: "Optional proposition classifier hint.", "x-visibleIf": { field: "mode", equals: "proposition" }, }, includePredictionMarkets: { type: "boolean", title: "Include prediction markets", description: "Attach bounded Polymarket context when available.", }, ingestOnRequest: { type: "boolean", title: "Ingest on request", description: "Ask gateway to perform request-time ingest if needed.", }, maxAgeHours: { type: "number", title: "Max age hours", description: "Lookback window for evidence search.", }, allocationMode: { type: "string", enum: ["fixed", "percent_equity"], title: "Allocation mode", description: "Position sizing method for execution-capable news strategies.", }, amountUsd: { type: "number", title: "Fixed amount", description: "USD notional per signal when allocation mode is fixed.", "x-visibleIf": { field: "allocationMode", equals: "fixed" }, }, percentOfEquity: { type: "number", title: "Percent of equity", description: "Target percent of account value per signal.", "x-visibleIf": { field: "allocationMode", equals: "percent_equity" }, }, maxPercentOfEquity: { type: "number", title: "Max percent of equity", description: "Upper cap for percent-based sizing.", "x-visibleIf": { field: "allocationMode", equals: "percent_equity" }, }, candidateLimit: { type: "number", title: "Candidate limit", description: "Max candidate events to consider for proposition mode.", "x-visibleIf": { field: "mode", equals: "proposition" }, }, resolution: { type: "string", title: "Price resolution", description: "Bar resolution used when mapping news checkpoints to market prices.", }, countBack: { type: "number", title: "Count back", description: "Max number of price bars to request when no explicit replay window is provided.", }, gate: { type: "object", title: "Gate", description: "Optional continuation gate evaluated against the fetched signal.", }, execution: { type: "object", title: "Execution", description: "Optional Hyperliquid execution settings for live runs.", }, tradePolicy: { type: "object", title: "Trade policy", description: "Maps event/proposition outcomes to buy and close behavior.", }, predictionMarketSignal: { type: "object", title: "Prediction market signal", description: "Optional direct Polymarket threshold rule for buy or sell decisions.", }, schedule: { type: "object", title: "Schedule", description: "Optional schedule for repeated reads.", }, }, }; function parseJson(raw: string | null): unknown | null { if (!raw) return null; try { return JSON.parse(raw) as unknown; } catch { return null; } } function pickDefined<T extends Record<string, unknown>>(value: T): Partial<T> { return Object.fromEntries( Object.entries(value).filter(([, entry]) => entry !== undefined), ) as Partial<T>; } function mergeDefined<T extends Record<string, unknown>>(base: T, overrides?: Partial<T>): T { if (!overrides) return base; return { ...base, ...pickDefined(overrides as Record<string, unknown>), } as T; } function resolveSchedule(config?: PartialScheduleConfig): ScheduleConfig { return mergeDefined(DEFAULT_SCHEDULE, config); } function resolveExecution(config?: PartialExecutionConfig): ExecutionConfig { const defaults: ExecutionConfig = { enabled: false, environment: DEFAULT_EXECUTION_ENV, mode: DEFAULT_EXECUTION_MODE, slippageBps: DEFAULT_SLIPPAGE_BPS, }; const merged = mergeDefined(defaults, config as Partial<ExecutionConfig> | undefined); return { enabled: merged.enabled, environment: merged.environment, mode: merged.mode, slippageBps: merged.slippageBps, ...(merged.symbol ? { symbol: merged.symbol } : {}), ...(typeof merged.size === "number" ? { size: merged.size } : {}), ...(typeof merged.leverage === "number" ? { leverage: merged.leverage } : {}), }; } function resolveTradePolicy(config?: PartialTradePolicyConfig): TradePolicyConfig { return { eventEntrySignals: config?.eventEntrySignals ?? DEFAULT_TRADE_POLICY.eventEntrySignals, eventExitSignals: config?.eventExitSignals ?? DEFAULT_TRADE_POLICY.eventExitSignals, propositionEntryAnswers: config?.propositionEntryAnswers ?? DEFAULT_TRADE_POLICY.propositionEntryAnswers, propositionExitAnswers: config?.propositionExitAnswers ?? DEFAULT_TRADE_POLICY.propositionExitAnswers, blockAction: config?.blockAction ?? DEFAULT_TRADE_POLICY.blockAction, }; } function mergeEventConfig( base: ParsedEventSimpleNewsBotConfig | EventSimpleNewsBotConfig | null, overrides: SimpleNewsBotRequestOverrides, ): EventSimpleNewsBotConfig { const gateOverrides = overrides.gate; const gateBase = base?.gate; const gate = gateOverrides || gateBase ? { mode: "event" as const, ...pickDefined((gateBase ?? {}) as Record<string, unknown>), ...pickDefined((gateOverrides ?? {}) as Record<string, unknown>), } : undefined; return eventConfigSchema.parse({ configVersion: TEMPLATE_CONFIG_VERSION, mode: "event", asset: base?.asset ?? TEMPLATE_CONFIG_DEFAULTS.asset, ...(base?.marketSymbol ? { marketSymbol: base.marketSymbol } : {}), includePredictionMarkets: base?.includePredictionMarkets ?? TEMPLATE_CONFIG_DEFAULTS.includePredictionMarkets, ingestOnRequest: base?.ingestOnRequest ?? TEMPLATE_CONFIG_DEFAULTS.ingestOnRequest, maxAgeHours: base?.maxAgeHours ?? TEMPLATE_CONFIG_DEFAULTS.maxAgeHours, allocationMode: base?.allocationMode ?? TEMPLATE_CONFIG_DEFAULTS.allocationMode, amountUsd: base?.amountUsd ?? TEMPLATE_CONFIG_DEFAULTS.amountUsd, percentOfEquity: base?.percentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.percentOfEquity, maxPercentOfEquity: base?.maxPercentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.maxPercentOfEquity, schedule: base?.schedule ?? TEMPLATE_CONFIG_DEFAULTS.schedule, resolution: base?.resolution ?? TEMPLATE_CONFIG_DEFAULTS.resolution, countBack: base?.countBack ?? TEMPLATE_CONFIG_DEFAULTS.countBack, execution: base?.execution ?? TEMPLATE_CONFIG_DEFAULTS.execution, tradePolicy: base?.tradePolicy ?? TEMPLATE_CONFIG_DEFAULTS.tradePolicy, ...(base?.predictionMarketSignal ? { predictionMarketSignal: base.predictionMarketSignal } : {}), ...(base?.query ? { query: base.query } : {}), ...(base?.eventKey ? { eventKey: base.eventKey } : {}), ...pickDefined({ asset: overrides.asset, marketSymbol: overrides.marketSymbol, query: overrides.query, eventKey: overrides.eventKey, includePredictionMarkets: overrides.includePredictionMarkets, ingestOnRequest: overrides.ingestOnRequest, maxAgeHours: overrides.maxAgeHours, allocationMode: overrides.allocationMode, amountUsd: overrides.amountUsd, percentOfEquity: overrides.percentOfEquity, maxPercentOfEquity: overrides.maxPercentOfEquity, resolution: overrides.resolution, countBack: overrides.countBack, execution: overrides.execution, tradePolicy: overrides.tradePolicy, predictionMarketSignal: overrides.predictionMarketSignal, }), ...(gate ? { gate } : {}), }) as EventSimpleNewsBotConfig; } function mergePropositionConfig( base: ParsedPropositionSimpleNewsBotConfig | PropositionSimpleNewsBotConfig | null, overrides: SimpleNewsBotRequestOverrides, ): PropositionSimpleNewsBotConfig { const gateOverrides = overrides.gate; const gateBase = base?.gate; const gate = gateOverrides || gateBase ? { mode: "proposition" as const, ...pickDefined((gateBase ?? {}) as Record<string, unknown>), ...pickDefined((gateOverrides ?? {}) as Record<string, unknown>), } : undefined; return propositionConfigSchema.parse({ configVersion: TEMPLATE_CONFIG_VERSION, mode: "proposition", question: base?.question ?? TEMPLATE_CONFIG_DEFAULTS.question, asset: base?.asset ?? TEMPLATE_CONFIG_DEFAULTS.asset, ...(base?.marketSymbol ? { marketSymbol: base.marketSymbol } : {}), includePredictionMarkets: base?.includePredictionMarkets ?? TEMPLATE_CONFIG_DEFAULTS.includePredictionMarkets, ingestOnRequest: base?.ingestOnRequest ?? TEMPLATE_CONFIG_DEFAULTS.ingestOnRequest, maxAgeHours: base?.maxAgeHours ?? TEMPLATE_CONFIG_DEFAULTS.maxAgeHours, allocationMode: base?.allocationMode ?? TEMPLATE_CONFIG_DEFAULTS.allocationMode, amountUsd: base?.amountUsd ?? TEMPLATE_CONFIG_DEFAULTS.amountUsd, percentOfEquity: base?.percentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.percentOfEquity, maxPercentOfEquity: base?.maxPercentOfEquity ?? TEMPLATE_CONFIG_DEFAULTS.maxPercentOfEquity, candidateLimit: base?.candidateLimit ?? TEMPLATE_CONFIG_DEFAULTS.candidateLimit, schedule: base?.schedule ?? TEMPLATE_CONFIG_DEFAULTS.schedule, resolution: base?.resolution ?? TEMPLATE_CONFIG_DEFAULTS.resolution, countBack: base?.countBack ?? TEMPLATE_CONFIG_DEFAULTS.countBack, execution: base?.execution ?? TEMPLATE_CONFIG_DEFAULTS.execution, tradePolicy: base?.tradePolicy ?? TEMPLATE_CONFIG_DEFAULTS.tradePolicy, ...(base?.predictionMarketSignal ? { predictionMarketSignal: base.predictionMarketSignal } : {}), ...(base?.query ? { query: base.query } : {}), ...(base?.eventKey ? { eventKey: base.eventKey } : {}), ...(base?.propositionType ? { propositionType: base.propositionType } : {}), ...pickDefined({ question: overrides.question, asset: overrides.asset, marketSymbol: overrides.marketSymbol, query: overrides.query, eventKey: overrides.eventKey, propositionType: overrides.propositionType, includePredictionMarkets: overrides.includePredictionMarkets, ingestOnRequest: overrides.ingestOnRequest, maxAgeHours: overrides.maxAgeHours, allocationMode: overrides.allocationMode, amountUsd: overrides.amountUsd, percentOfEquity: overrides.percentOfEquity, maxPercentOfEquity: overrides.maxPercentOfEquity, candidateLimit: overrides.candidateLimit, resolution: overrides.resolution, countBack: overrides.countBack, execution: overrides.execution, tradePolicy: overrides.tradePolicy, predictionMarketSignal: overrides.predictionMarketSignal, }), ...(gate ? { gate } : {}), }) as PropositionSimpleNewsBotConfig; } export function readConfig(): SimpleNewsBotConfig { const parsed = simpleNewsBotConfigSchema.safeParse( parseJson(process.env[CONFIG_ENV] ?? null) ?? TEMPLATE_CONFIG_DEFAULTS, ); const base = parsed.success ? parsed.data : TEMPLATE_CONFIG_DEFAULTS; if (base.mode === "event") { return { ...mergeEventConfig(base, {}), schedule: resolveSchedule(base.schedule), execution: resolveExecution(base.execution), tradePolicy: resolveTradePolicy(base.tradePolicy), }; } return { ...mergePropositionConfig(base, {}), schedule: resolveSchedule(base.schedule), execution: resolveExecution(base.execution), tradePolicy: resolveTradePolicy(base.tradePolicy), }; } export function resolveRuntimeConfig( overrides: SimpleNewsBotRequestOverrides = {}, ): SimpleNewsBotConfig { const base = readConfig(); const mode = overrides.mode ?? base.mode; if (mode === "event") { const merged = mergeEventConfig(base.mode === "event" ? base : null, overrides); return { ...merged, schedule: resolveSchedule(merged.schedule), execution: resolveExecution(mergeDefined(base.execution, overrides.execution)), tradePolicy: resolveTradePolicy(mergeDefined(base.tradePolicy, overrides.tradePolicy)), }; } const merged = mergePropositionConfig(base.mode === "proposition" ? base : null, overrides); return { ...merged, schedule: resolveSchedule(merged.schedule), execution: resolveExecution(mergeDefined(base.execution, overrides.execution)), tradePolicy: resolveTradePolicy(mergeDefined(base.tradePolicy, overrides.tradePolicy)), }; } export function resolveScheduleConfig(config: SimpleNewsBotConfig) { return config.schedule ?? DEFAULT_SCHEDULE; } function resolveProfileAssetSymbols(config: SimpleNewsBotConfig): string[] { const fallback = resolveConfiguredMarketSymbol(config); const normalized = normalizeHyperliquidBaseSymbol(fallback); return normalized ? [normalized] : []; } export function resolveConfiguredMarketSymbol(config: SimpleNewsBotConfig): string { const marketSymbol = config.marketSymbol?.trim(); if (marketSymbol) return marketSymbol; const executionSymbol = config.execution?.symbol?.trim(); if (executionSymbol) return executionSymbol; return config.asset.trim() || DEFAULT_ASSET; } export function resolveProfileAssets(config: SimpleNewsBotConfig): Array<{ venue: "hyperliquid"; chain: "hyperliquid" | "hyperliquid-testnet"; assetSymbols: string[]; pair?: string; leverage?: number; }> { const symbols = resolveProfileAssetSymbols(config); if (symbols.length === 0) return []; return buildHyperliquidProfileAssets({ environment: config.execution?.environment ?? DEFAULT_EXECUTION_ENV, assets: [ { assetSymbols: symbols, leverage: config.execution?.leverage, }, ], }); } export const NEWS_BOT_TEMPLATE_CONFIG = { version: TEMPLATE_CONFIG_VERSION, schema: TEMPLATE_CONFIG_SCHEMA, defaults: TEMPLATE_CONFIG_DEFAULTS, envVar: TEMPLATE_CONFIG_ENV_VAR, };