openpondai/agents/news-bot
OpenTool app
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,
};